@oxy.so/core 4.5.0-oxy-one.1 → 4.6.0

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.
Files changed (283) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/HttpService.js +63 -19
  3. package/dist/cjs/api/accounts.js +10 -3
  4. package/dist/cjs/api/agency.js +3 -1
  5. package/dist/cjs/api/apps.js +15 -4
  6. package/dist/cjs/api/assets.js +39 -11
  7. package/dist/cjs/api/auth.js +28 -8
  8. package/dist/cjs/api/billing.js +24 -7
  9. package/dist/cjs/api/civic.js +14 -4
  10. package/dist/cjs/api/follows.js +9 -2
  11. package/dist/cjs/api/identity.js +31 -9
  12. package/dist/cjs/api/nodes.js +3 -1
  13. package/dist/cjs/api/privacy.js +8 -2
  14. package/dist/cjs/api/session.js +15 -4
  15. package/dist/cjs/api/store.js +15 -5
  16. package/dist/cjs/api/topics.js +10 -3
  17. package/dist/cjs/api/users.js +16 -4
  18. package/dist/cjs/boot/sessionColdBoot.js +40 -13
  19. package/dist/cjs/constants/version.js +6 -6
  20. package/dist/cjs/crypto/identityMarker.js +3 -1
  21. package/dist/cjs/crypto/identityProof.js +6 -1
  22. package/dist/cjs/crypto/keyManager.js +87 -32
  23. package/dist/cjs/crypto/recoveryPhrase.js +2 -2
  24. package/dist/cjs/crypto/signatureService.js +3 -4
  25. package/dist/cjs/i18n/index.js +16 -3
  26. package/dist/cjs/i18n/locales/ar-SA.js +366 -366
  27. package/dist/cjs/i18n/locales/ca-ES.js +366 -366
  28. package/dist/cjs/i18n/locales/de-DE.js +366 -366
  29. package/dist/cjs/i18n/locales/en-US.js +1994 -1994
  30. package/dist/cjs/i18n/locales/es-ES.js +1994 -1994
  31. package/dist/cjs/i18n/locales/fr-FR.js +366 -366
  32. package/dist/cjs/i18n/locales/it-IT.js +366 -366
  33. package/dist/cjs/i18n/locales/ja-JP.js +366 -366
  34. package/dist/cjs/i18n/locales/ko-KR.js +366 -366
  35. package/dist/cjs/i18n/locales/pt-PT.js +366 -366
  36. package/dist/cjs/i18n/locales/zh-CN.js +366 -366
  37. package/dist/cjs/inference/OxyInferenceClient.js +12 -18
  38. package/dist/cjs/models/interfaces.js +11 -11
  39. package/dist/cjs/server/OxyServer.js +86 -28
  40. package/dist/cjs/server/agentAccount.js +6 -2
  41. package/dist/cjs/server/aliaMachineCredential.js +39 -14
  42. package/dist/cjs/server/auth.js +5 -4
  43. package/dist/cjs/server/capabilityTicket.js +5 -5
  44. package/dist/cjs/server/index.js +2 -1
  45. package/dist/cjs/server/liveCapabilityTicket.js +4 -1
  46. package/dist/cjs/server/middleware.js +61 -42
  47. package/dist/cjs/server/namespaces.js +28 -7
  48. package/dist/cjs/server/requesterAssertion.js +36 -30
  49. package/dist/cjs/server/serviceTokenLifetime.js +7 -4
  50. package/dist/cjs/server/traffic.js +53 -14
  51. package/dist/cjs/server/trafficNodeHttp.js +33 -8
  52. package/dist/cjs/server/workloadIdentity.client.js +4 -0
  53. package/dist/cjs/server/workloadIdentity.js +28 -1
  54. package/dist/cjs/session/SessionClient.js +54 -17
  55. package/dist/cjs/session/accountDelegationAccess.js +4 -6
  56. package/dist/cjs/session/accountDialogController.js +1 -1
  57. package/dist/cjs/session/authStateStore.js +18 -9
  58. package/dist/cjs/session/identityPin.js +6 -2
  59. package/dist/cjs/session/refresh.js +81 -23
  60. package/dist/cjs/session/sharedDeviceCredential.js +28 -15
  61. package/dist/cjs/shared/utils/errorUtils.js +1 -1
  62. package/dist/cjs/shared/utils/networkUtils.js +1 -1
  63. package/dist/cjs/shared/utils/themeUtils.js +1 -1
  64. package/dist/cjs/utils/accountCacheSweep.js +3 -1
  65. package/dist/cjs/utils/accountUtils.js +1 -3
  66. package/dist/cjs/utils/asyncUtils.js +1 -1
  67. package/dist/cjs/utils/authHelpers.js +2 -1
  68. package/dist/cjs/utils/avatarUtils.js +4 -2
  69. package/dist/cjs/utils/cache.js +1 -1
  70. package/dist/cjs/utils/cacheKey.js +3 -1
  71. package/dist/cjs/utils/deviceManager.js +4 -4
  72. package/dist/cjs/utils/errorUtils.js +6 -4
  73. package/dist/cjs/utils/languageUtils.js +404 -57
  74. package/dist/cjs/utils/profileLinks.js +1 -3
  75. package/dist/cjs/utils/requestUtils.js +7 -1
  76. package/dist/cjs/utils/userIdentity.js +1 -1
  77. package/dist/cjs/utils/validationUtils.js +5 -6
  78. package/dist/esm/HttpService.js +64 -20
  79. package/dist/esm/api/accounts.js +11 -4
  80. package/dist/esm/api/agency.js +3 -1
  81. package/dist/esm/api/apps.js +15 -4
  82. package/dist/esm/api/assets.js +39 -11
  83. package/dist/esm/api/auth.js +28 -8
  84. package/dist/esm/api/billing.js +25 -8
  85. package/dist/esm/api/civic.js +14 -4
  86. package/dist/esm/api/follows.js +9 -2
  87. package/dist/esm/api/identity.js +31 -9
  88. package/dist/esm/api/nodes.js +3 -1
  89. package/dist/esm/api/privacy.js +8 -2
  90. package/dist/esm/api/session.js +15 -4
  91. package/dist/esm/api/store.js +15 -5
  92. package/dist/esm/api/topics.js +10 -3
  93. package/dist/esm/api/users.js +16 -4
  94. package/dist/esm/boot/sessionColdBoot.js +41 -14
  95. package/dist/esm/constants/version.js +6 -6
  96. package/dist/esm/crypto/identityMarker.js +3 -1
  97. package/dist/esm/crypto/identityProof.js +6 -1
  98. package/dist/esm/crypto/internal.js +1 -1
  99. package/dist/esm/crypto/keyManager.js +87 -32
  100. package/dist/esm/crypto/recoveryPhrase.js +2 -2
  101. package/dist/esm/crypto/signatureService.js +3 -4
  102. package/dist/esm/i18n/index.js +16 -3
  103. package/dist/esm/i18n/locales/ar-SA.js +366 -366
  104. package/dist/esm/i18n/locales/ca-ES.js +366 -366
  105. package/dist/esm/i18n/locales/de-DE.js +366 -366
  106. package/dist/esm/i18n/locales/en-US.js +1994 -1994
  107. package/dist/esm/i18n/locales/es-ES.js +1994 -1994
  108. package/dist/esm/i18n/locales/fr-FR.js +366 -366
  109. package/dist/esm/i18n/locales/it-IT.js +366 -366
  110. package/dist/esm/i18n/locales/ja-JP.js +366 -366
  111. package/dist/esm/i18n/locales/ko-KR.js +366 -366
  112. package/dist/esm/i18n/locales/pt-PT.js +366 -366
  113. package/dist/esm/i18n/locales/zh-CN.js +366 -366
  114. package/dist/esm/index.js +1 -1
  115. package/dist/esm/inference/OxyInferenceClient.js +13 -19
  116. package/dist/esm/models/interfaces.js +11 -11
  117. package/dist/esm/server/OxyServer.js +87 -29
  118. package/dist/esm/server/agentAccount.js +6 -2
  119. package/dist/esm/server/aliaMachineCredential.js +39 -14
  120. package/dist/esm/server/auth.js +6 -5
  121. package/dist/esm/server/capabilityTicket.js +6 -6
  122. package/dist/esm/server/index.js +5 -5
  123. package/dist/esm/server/liveCapabilityTicket.js +6 -3
  124. package/dist/esm/server/middleware.js +62 -43
  125. package/dist/esm/server/namespaces.js +31 -10
  126. package/dist/esm/server/requesterAssertion.js +36 -30
  127. package/dist/esm/server/serviceTokenLifetime.js +7 -4
  128. package/dist/esm/server/traffic.js +55 -16
  129. package/dist/esm/server/trafficNodeHttp.js +34 -9
  130. package/dist/esm/server/userInvalidation.js +1 -1
  131. package/dist/esm/server/workloadIdentity.client.js +3 -0
  132. package/dist/esm/server/workloadIdentity.js +27 -1
  133. package/dist/esm/session/SessionClient.js +54 -17
  134. package/dist/esm/session/accountDelegationAccess.js +5 -7
  135. package/dist/esm/session/accountDialogController.js +1 -1
  136. package/dist/esm/session/authStateStore.js +18 -9
  137. package/dist/esm/session/identityPin.js +6 -2
  138. package/dist/esm/session/refresh.js +82 -24
  139. package/dist/esm/session/sharedDeviceCredential.js +28 -15
  140. package/dist/esm/shared/utils/errorUtils.js +1 -1
  141. package/dist/esm/shared/utils/networkUtils.js +1 -1
  142. package/dist/esm/shared/utils/themeUtils.js +1 -1
  143. package/dist/esm/utils/accountCacheSweep.js +3 -1
  144. package/dist/esm/utils/accountUtils.js +1 -3
  145. package/dist/esm/utils/asyncUtils.js +1 -1
  146. package/dist/esm/utils/authHelpers.js +2 -1
  147. package/dist/esm/utils/avatarUtils.js +4 -2
  148. package/dist/esm/utils/cache.js +1 -1
  149. package/dist/esm/utils/cacheKey.js +3 -1
  150. package/dist/esm/utils/deviceManager.js +4 -4
  151. package/dist/esm/utils/errorUtils.js +6 -4
  152. package/dist/esm/utils/languageUtils.js +404 -57
  153. package/dist/esm/utils/profileLinks.js +1 -3
  154. package/dist/esm/utils/requestUtils.js +7 -1
  155. package/dist/esm/utils/userIdentity.js +1 -1
  156. package/dist/esm/utils/validationUtils.js +5 -6
  157. package/dist/types/HttpService.d.ts +1 -1
  158. package/dist/types/api/accounts.d.ts +16 -3
  159. package/dist/types/api/apps.d.ts +4 -4
  160. package/dist/types/crypto/internal.d.ts +1 -1
  161. package/dist/types/index.d.ts +4 -4
  162. package/dist/types/models/interfaces.d.ts +9 -1
  163. package/dist/types/server/index.d.ts +6 -6
  164. package/dist/types/server/workloadIdentity.client.d.ts +5 -1
  165. package/dist/types/server/workloadIdentity.d.ts +26 -0
  166. package/dist/types/session/accountDelegationAccess.d.ts +1 -1
  167. package/dist/types/session/index.d.ts +1 -1
  168. package/dist/types/utils/userIdentity.d.ts +1 -1
  169. package/dist/types/utils/validationUtils.d.ts +4 -5
  170. package/package.json +3 -4
  171. package/src/HttpService.ts +183 -53
  172. package/src/OxyServices.errors.ts +7 -3
  173. package/src/OxyServices.ts +6 -2
  174. package/src/api/accounts.ts +96 -28
  175. package/src/api/agency.ts +24 -9
  176. package/src/api/appData.ts +12 -2
  177. package/src/api/apps.ts +81 -31
  178. package/src/api/assets.ts +113 -43
  179. package/src/api/auth.ts +145 -77
  180. package/src/api/billing.ts +112 -65
  181. package/src/api/civic.ts +61 -19
  182. package/src/api/contacts.ts +4 -1
  183. package/src/api/devices.ts +54 -21
  184. package/src/api/follows.ts +88 -31
  185. package/src/api/identity.ts +101 -33
  186. package/src/api/linkedAccounts.ts +26 -8
  187. package/src/api/nodes.ts +18 -6
  188. package/src/api/notifications.ts +26 -6
  189. package/src/api/privacy.ts +42 -8
  190. package/src/api/reputation.ts +18 -7
  191. package/src/api/session.ts +23 -5
  192. package/src/api/store.ts +81 -31
  193. package/src/api/topics.ts +18 -6
  194. package/src/api/users.ts +98 -35
  195. package/src/boot/sessionColdBoot.ts +60 -19
  196. package/src/civic/payloads.ts +1 -4
  197. package/src/client/context.ts +6 -1
  198. package/src/constants/version.ts +7 -6
  199. package/src/crypto/aead.ts +1 -5
  200. package/src/crypto/ecdh.ts +1 -4
  201. package/src/crypto/identityDerivations.ts +5 -1
  202. package/src/crypto/identityMarker.ts +3 -1
  203. package/src/crypto/identityProof.ts +6 -1
  204. package/src/crypto/index.ts +11 -11
  205. package/src/crypto/internal.ts +6 -1
  206. package/src/crypto/keyManager.ts +348 -98
  207. package/src/crypto/recoveryPhrase.ts +14 -9
  208. package/src/crypto/registrationPow.ts +1 -1
  209. package/src/crypto/signatureService.ts +14 -6
  210. package/src/i18n/accountCategoryLabels.ts +1 -4
  211. package/src/i18n/accountRoleLabels.ts +1 -4
  212. package/src/i18n/index.ts +25 -7
  213. package/src/i18n/locales/ar-SA.ts +375 -366
  214. package/src/i18n/locales/ca-ES.ts +382 -366
  215. package/src/i18n/locales/de-DE.ts +387 -366
  216. package/src/i18n/locales/en-US.ts +2034 -1994
  217. package/src/i18n/locales/es-ES.ts +2043 -1994
  218. package/src/i18n/locales/fr-FR.ts +387 -366
  219. package/src/i18n/locales/it-IT.ts +380 -366
  220. package/src/i18n/locales/ja-JP.ts +380 -366
  221. package/src/i18n/locales/ko-KR.ts +377 -366
  222. package/src/i18n/locales/pt-PT.ts +383 -366
  223. package/src/i18n/locales/zh-CN.ts +373 -366
  224. package/src/i18n/trustTierLabels.ts +1 -4
  225. package/src/index.ts +371 -349
  226. package/src/inference/OxyInferenceClient.ts +784 -780
  227. package/src/models/interfaces.ts +39 -27
  228. package/src/server/OxyServer.ts +227 -68
  229. package/src/server/agentAccount.ts +8 -2
  230. package/src/server/aliaMachineCredential.ts +57 -16
  231. package/src/server/auth.ts +9 -5
  232. package/src/server/capabilityTicket.ts +30 -21
  233. package/src/server/index.ts +29 -6
  234. package/src/server/liveCapabilityTicket.ts +31 -9
  235. package/src/server/middleware.ts +212 -121
  236. package/src/server/namespaces.ts +247 -94
  237. package/src/server/rateLimit.ts +2 -4
  238. package/src/server/requesterAssertion.ts +93 -54
  239. package/src/server/serviceTokenLifetime.ts +9 -4
  240. package/src/server/traffic.ts +117 -23
  241. package/src/server/trafficNodeHttp.ts +61 -14
  242. package/src/server/userInvalidation.ts +1 -4
  243. package/src/server/workloadIdentity.client.ts +12 -1
  244. package/src/server/workloadIdentity.ts +52 -5
  245. package/src/session/SessionClient.ts +170 -49
  246. package/src/session/accountDelegationAccess.ts +24 -31
  247. package/src/session/accountDialogController.ts +59 -14
  248. package/src/session/accountSwitchTargets.ts +10 -15
  249. package/src/session/authStateStore.ts +121 -81
  250. package/src/session/deviceDirectory.ts +1 -3
  251. package/src/session/identityPin.ts +10 -11
  252. package/src/session/identitySession.ts +3 -1
  253. package/src/session/index.ts +87 -82
  254. package/src/session/refresh.ts +138 -44
  255. package/src/session/sessionClientHost.ts +1 -3
  256. package/src/session/sharedDeviceCredential.ts +35 -14
  257. package/src/session/socketLoader.ts +9 -2
  258. package/src/shared/utils/errorUtils.ts +5 -2
  259. package/src/shared/utils/networkUtils.ts +5 -8
  260. package/src/shared/utils/themeUtils.ts +2 -6
  261. package/src/types/buffer.d.ts +66 -21
  262. package/src/types/expo-secure-store.d.ts +5 -1
  263. package/src/utils/accountCacheSweep.ts +4 -5
  264. package/src/utils/accountUtils.ts +107 -108
  265. package/src/utils/apiUtils.ts +3 -3
  266. package/src/utils/asyncUtils.ts +2 -2
  267. package/src/utils/authHelpers.ts +10 -5
  268. package/src/utils/avatarUtils.ts +11 -6
  269. package/src/utils/cache.ts +9 -10
  270. package/src/utils/cacheKey.ts +5 -1
  271. package/src/utils/coldBoot.ts +1 -3
  272. package/src/utils/commonsDelivery.ts +1 -4
  273. package/src/utils/deviceManager.ts +15 -14
  274. package/src/utils/errorUtils.ts +68 -86
  275. package/src/utils/languageUtils.ts +407 -58
  276. package/src/utils/oauthPkce.ts +1 -4
  277. package/src/utils/platform.ts +0 -1
  278. package/src/utils/profileLinks.ts +2 -5
  279. package/src/utils/requestUtils.ts +18 -14
  280. package/src/utils/sessionUtils.ts +34 -33
  281. package/src/utils/userHandle.ts +3 -1
  282. package/src/utils/userIdentity.ts +1 -1
  283. package/src/utils/validationUtils.ts +11 -9
@@ -51,36 +51,42 @@ import type { MeteredGeneration } from '@oxy.so/contracts';
51
51
  * request.
52
52
  */
53
53
 
54
- import { decisionRequestSchema, decisionSuccessSchema, decisionAnswersMatch, type DecisionRequest, type DecisionSuccess } from '@oxy.so/contracts';
54
+ import {
55
+ decisionRequestSchema,
56
+ decisionSuccessSchema,
57
+ decisionAnswersMatch,
58
+ type DecisionRequest,
59
+ type DecisionSuccess,
60
+ } from '@oxy.so/contracts';
55
61
 
56
62
  import type {
57
- CurrencyCode,
58
- ExactDecimal,
59
- InferenceEnvironment,
60
- InferenceErrorCode,
61
- InferenceFinishReason,
62
- InferenceMessage,
63
- InferenceReasoning,
64
- InferenceRequestOutcome,
65
- InferenceAudioMediaType,
66
- InferenceStreamEvent,
67
- InferenceSpeechParameters,
68
- ModelCatalogueEntry,
69
- ResponseFormat,
70
- RoutingPolicyReference,
71
- RoutingProfile,
72
- ToolChoice,
73
- ToolDefinition,
74
- UnitPrice,
75
- UsageQuantity,
76
- UsageSource,
63
+ CurrencyCode,
64
+ ExactDecimal,
65
+ InferenceEnvironment,
66
+ InferenceErrorCode,
67
+ InferenceFinishReason,
68
+ InferenceMessage,
69
+ InferenceReasoning,
70
+ InferenceRequestOutcome,
71
+ InferenceAudioMediaType,
72
+ InferenceStreamEvent,
73
+ InferenceSpeechParameters,
74
+ ModelCatalogueEntry,
75
+ ResponseFormat,
76
+ RoutingPolicyReference,
77
+ RoutingProfile,
78
+ ToolChoice,
79
+ ToolDefinition,
80
+ UnitPrice,
81
+ UsageQuantity,
82
+ UsageSource,
77
83
  } from '@oxy.so/contracts';
78
84
  import {
79
- INFERENCE_ERROR_CODES,
80
- MAX_INFERENCE_AUDIO_BYTES,
81
- inferenceAudioMediaTypeSchema,
82
- inferenceStreamEventSchema,
83
- modelIdSchema,
85
+ INFERENCE_ERROR_CODES,
86
+ MAX_INFERENCE_AUDIO_BYTES,
87
+ inferenceAudioMediaTypeSchema,
88
+ inferenceStreamEventSchema,
89
+ modelIdSchema,
84
90
  } from '@oxy.so/contracts';
85
91
 
86
92
  /** The base URL of the Oxy API, when a caller names none. */
@@ -94,9 +100,7 @@ export const OXY_INFERENCE_BASE_URL = 'https://api.oxy.so';
94
100
  * lane and is called on EVERY request, because a session bearer and a service
95
101
  * token both rotate and a captured one goes stale inside the hour.
96
102
  */
97
- export type OxyInferenceCredential =
98
- | string
99
- | (() => string | null | Promise<string | null>);
103
+ export type OxyInferenceCredential = string | (() => string | null | Promise<string | null>);
100
104
 
101
105
  /**
102
106
  * The `fetch` this client calls.
@@ -108,11 +112,11 @@ export type OxyInferenceCredential =
108
112
  export type OxyInferenceFetch = typeof fetch;
109
113
 
110
114
  export interface OxyInferenceClientOptions {
111
- readonly credential: OxyInferenceCredential;
112
- /** Defaults to {@link OXY_INFERENCE_BASE_URL}. A trailing slash is trimmed. */
113
- readonly baseURL?: string;
114
- /** Defaults to the global `fetch`. */
115
- readonly fetch?: OxyInferenceFetch;
115
+ readonly credential: OxyInferenceCredential;
116
+ /** Defaults to {@link OXY_INFERENCE_BASE_URL}. A trailing slash is trimmed. */
117
+ readonly baseURL?: string;
118
+ /** Defaults to the global `fetch`. */
119
+ readonly fetch?: OxyInferenceFetch;
116
120
  }
117
121
 
118
122
  /**
@@ -125,109 +129,109 @@ export interface OxyInferenceClientOptions {
125
129
  * both is refused by the edge with `invalid_request`.
126
130
  */
127
131
  export interface OxyResponsesRequest {
128
- /** `<publisher>/<model>` or `<publisher>/<model>@<revision>`. */
129
- readonly model?: string;
130
- /** A routing profile slug. Never contains a slash, so it is never a model id. */
131
- readonly routingProfile?: string;
132
- /** An exact opaque routing-profile database ID; serialized byte-for-byte. */
133
- readonly routingProfileId?: string;
134
- /** A prompt, or the message list it is shorthand for. */
135
- readonly input: string | readonly InferenceMessage[];
136
- readonly maxOutputTokens?: number;
137
- readonly temperature?: number;
138
- readonly topP?: number;
139
- readonly topK?: number;
140
- readonly frequencyPenalty?: number;
141
- readonly presencePenalty?: number;
142
- readonly seed?: number;
143
- readonly stopSequences?: readonly string[];
144
- readonly tools?: readonly ToolDefinition[];
145
- readonly toolChoice?: ToolChoice;
146
- readonly responseFormat?: ResponseFormat;
147
- /**
148
- * `{ effort: 'low' | 'medium' | 'high' }`. Name only an effort the model
149
- * lists in its catalogue entry's `capabilities.reasoningEfforts`; any other
150
- * is refused with `invalid_request` (HTTP 400) before anything is charged.
151
- */
152
- readonly reasoning?: InferenceReasoning;
153
- /** Cost-attribution tags, echoed back on the receipt. At most 16. */
154
- readonly labels?: Readonly<Record<string, string>>;
155
- /** Your own correlation id, echoed on the response. */
156
- readonly clientRequestId?: string;
132
+ /** `<publisher>/<model>` or `<publisher>/<model>@<revision>`. */
133
+ readonly model?: string;
134
+ /** A routing profile slug. Never contains a slash, so it is never a model id. */
135
+ readonly routingProfile?: string;
136
+ /** An exact opaque routing-profile database ID; serialized byte-for-byte. */
137
+ readonly routingProfileId?: string;
138
+ /** A prompt, or the message list it is shorthand for. */
139
+ readonly input: string | readonly InferenceMessage[];
140
+ readonly maxOutputTokens?: number;
141
+ readonly temperature?: number;
142
+ readonly topP?: number;
143
+ readonly topK?: number;
144
+ readonly frequencyPenalty?: number;
145
+ readonly presencePenalty?: number;
146
+ readonly seed?: number;
147
+ readonly stopSequences?: readonly string[];
148
+ readonly tools?: readonly ToolDefinition[];
149
+ readonly toolChoice?: ToolChoice;
150
+ readonly responseFormat?: ResponseFormat;
151
+ /**
152
+ * `{ effort: 'low' | 'medium' | 'high' }`. Name only an effort the model
153
+ * lists in its catalogue entry's `capabilities.reasoningEfforts`; any other
154
+ * is refused with `invalid_request` (HTTP 400) before anything is charged.
155
+ */
156
+ readonly reasoning?: InferenceReasoning;
157
+ /** Cost-attribution tags, echoed back on the receipt. At most 16. */
158
+ readonly labels?: Readonly<Record<string, string>>;
159
+ /** Your own correlation id, echoed on the response. */
160
+ readonly clientRequestId?: string;
157
161
  }
158
162
 
159
163
  /** Speech uses the same credential and delegation lane as responses. */
160
164
  export type OxySpeechRequest = (
161
- | { readonly model: string; readonly routingProfileId?: never }
162
- | { readonly routingProfileId: string; readonly model?: never }
165
+ | { readonly model: string; readonly routingProfileId?: never }
166
+ | { readonly routingProfileId: string; readonly model?: never }
163
167
  ) & {
164
- readonly input: string;
165
- readonly voice: string;
166
- readonly response_format?: InferenceSpeechParameters['responseFormat'];
167
- readonly speed?: number;
168
+ readonly input: string;
169
+ readonly voice: string;
170
+ readonly response_format?: InferenceSpeechParameters['responseFormat'];
171
+ readonly speed?: number;
168
172
  };
169
173
 
170
174
  export interface OxySpeechResponse {
171
- readonly audio: Uint8Array;
172
- readonly mediaType: InferenceAudioMediaType;
173
- readonly requestId: string;
175
+ readonly audio: Uint8Array;
176
+ readonly mediaType: InferenceAudioMediaType;
177
+ readonly requestId: string;
174
178
  }
175
179
 
176
180
  export interface OxyInferenceRequestOptions {
177
- /**
178
- * Abort the request. The edge treats a client disconnect as a cancellation:
179
- * it settles what was produced and refunds the rest, so a cancelled request
180
- * is a normal terminal state rather than an error to clean up after.
181
- */
182
- readonly signal?: AbortSignal;
183
- /**
184
- * `Idempotency-Key`. A key already bound to a reservation is REFUSED with
185
- * `idempotency_conflict` rather than replayed — responses are not retained,
186
- * so there is nothing to replay, and refusing is what makes "a retry never
187
- * produces a second charge" structural. At most 128 characters.
188
- */
189
- readonly idempotencyKey?: string;
190
- /**
191
- * `X-Oxy-User-Id` — the end user this request is made on behalf of.
192
- * ATTRIBUTION ONLY: it never changes which account is charged.
193
- */
194
- readonly delegatedUserId?: string;
181
+ /**
182
+ * Abort the request. The edge treats a client disconnect as a cancellation:
183
+ * it settles what was produced and refunds the rest, so a cancelled request
184
+ * is a normal terminal state rather than an error to clean up after.
185
+ */
186
+ readonly signal?: AbortSignal;
187
+ /**
188
+ * `Idempotency-Key`. A key already bound to a reservation is REFUSED with
189
+ * `idempotency_conflict` rather than replayed — responses are not retained,
190
+ * so there is nothing to replay, and refusing is what makes "a retry never
191
+ * produces a second charge" structural. At most 128 characters.
192
+ */
193
+ readonly idempotencyKey?: string;
194
+ /**
195
+ * `X-Oxy-User-Id` — the end user this request is made on behalf of.
196
+ * ATTRIBUTION ONLY: it never changes which account is charged.
197
+ */
198
+ readonly delegatedUserId?: string;
195
199
  }
196
200
 
197
201
  /** The body of a successful `POST /v1/responses`. */
198
202
  export interface OxyInferenceResponse {
199
- readonly schemaVersion: 1;
200
- /** Also on `X-Oxy-Request-Id`, on success and on every refusal. */
201
- readonly requestId: string;
202
- readonly generationId?: string;
203
- /** Always revision-pinned, even when you named only the model line. */
204
- readonly model: string;
205
- readonly servingProvider: string;
206
- readonly finishReason: InferenceFinishReason;
207
- readonly output: readonly InferenceMessage[];
208
- /** Metered quantities. Never money — the charge is on the receipt. */
209
- readonly usage: readonly UsageQuantity[];
210
- /** The exact policy version this request was admitted under. */
211
- readonly routingPolicy: RoutingPolicyReference;
212
- /**
213
- * How long Oxy took over this request, in whole milliseconds — also on
214
- * `X-Oxy-Latency-Ms`.
215
- *
216
- * Measured from the moment the edge received the request through
217
- * authentication, admission, routing, the reservation, the call to the
218
- * inference data plane and the settlement of the hold. Most of it is the
219
- * upstream generating tokens; it does not separate the two.
220
- *
221
- * It is NOT the round trip you can measure yourself, which additionally
222
- * covers DNS, TLS, both network legs and your own parse. Report them side by
223
- * side rather than picking one — this figure has no network in it and yours
224
- * cannot be attributed to the model.
225
- *
226
- * Optional because it is additive: an Oxy deployment older than the field
227
- * omits it, and a streamed request never carries it (the head is written
228
- * before the first frame arrives, so the number does not exist yet).
229
- */
230
- readonly latencyMs?: number;
203
+ readonly schemaVersion: 1;
204
+ /** Also on `X-Oxy-Request-Id`, on success and on every refusal. */
205
+ readonly requestId: string;
206
+ readonly generationId?: string;
207
+ /** Always revision-pinned, even when you named only the model line. */
208
+ readonly model: string;
209
+ readonly servingProvider: string;
210
+ readonly finishReason: InferenceFinishReason;
211
+ readonly output: readonly InferenceMessage[];
212
+ /** Metered quantities. Never money — the charge is on the receipt. */
213
+ readonly usage: readonly UsageQuantity[];
214
+ /** The exact policy version this request was admitted under. */
215
+ readonly routingPolicy: RoutingPolicyReference;
216
+ /**
217
+ * How long Oxy took over this request, in whole milliseconds — also on
218
+ * `X-Oxy-Latency-Ms`.
219
+ *
220
+ * Measured from the moment the edge received the request through
221
+ * authentication, admission, routing, the reservation, the call to the
222
+ * inference data plane and the settlement of the hold. Most of it is the
223
+ * upstream generating tokens; it does not separate the two.
224
+ *
225
+ * It is NOT the round trip you can measure yourself, which additionally
226
+ * covers DNS, TLS, both network legs and your own parse. Report them side by
227
+ * side rather than picking one — this figure has no network in it and yours
228
+ * cannot be attributed to the model.
229
+ *
230
+ * Optional because it is additive: an Oxy deployment older than the field
231
+ * omits it, and a streamed request never carries it (the head is written
232
+ * before the first frame arrives, so the number does not exist yet).
233
+ */
234
+ readonly latencyMs?: number;
231
235
  }
232
236
 
233
237
  /**
@@ -237,31 +241,31 @@ export interface OxyInferenceResponse {
237
241
  * arithmetic stays checkable after that version has been superseded.
238
242
  */
239
243
  export interface OxyGenerationReceipt {
240
- readonly schemaVersion: 1;
241
- readonly receiptId: string;
242
- readonly requestId: string;
243
- readonly generationId?: string;
244
- readonly applicationId: string;
245
- readonly credentialId: string;
246
- /** Attribution only. Never the billing identity. */
247
- readonly delegatedUserId?: string;
248
- readonly environment: InferenceEnvironment;
249
- readonly outcome: InferenceRequestOutcome;
250
- readonly usageSource: UsageSource;
251
- /** EVERY unit, including the zeros — see `usageSource` for what a zero means. */
252
- readonly units: readonly UsageQuantity[];
253
- readonly resolvedModelReference: string;
254
- readonly servingProvider: string;
255
- readonly priceSnapshot: {
256
- readonly priceVersionId: string;
257
- readonly currency: CurrencyCode;
258
- readonly unitPrices: readonly UnitPrice[];
259
- };
260
- readonly billedAmount: ExactDecimal;
244
+ readonly schemaVersion: 1;
245
+ readonly receiptId: string;
246
+ readonly requestId: string;
247
+ readonly generationId?: string;
248
+ readonly applicationId: string;
249
+ readonly credentialId: string;
250
+ /** Attribution only. Never the billing identity. */
251
+ readonly delegatedUserId?: string;
252
+ readonly environment: InferenceEnvironment;
253
+ readonly outcome: InferenceRequestOutcome;
254
+ readonly usageSource: UsageSource;
255
+ /** EVERY unit, including the zeros — see `usageSource` for what a zero means. */
256
+ readonly units: readonly UsageQuantity[];
257
+ readonly resolvedModelReference: string;
258
+ readonly servingProvider: string;
259
+ readonly priceSnapshot: {
260
+ readonly priceVersionId: string;
261
261
  readonly currency: CurrencyCode;
262
- /** A BYOK route: `billedAmount` is Oxy's fee, not the cost of the tokens. */
263
- readonly platformFeeOnly: boolean;
264
- readonly settledAt: string;
262
+ readonly unitPrices: readonly UnitPrice[];
263
+ };
264
+ readonly billedAmount: ExactDecimal;
265
+ readonly currency: CurrencyCode;
266
+ /** A BYOK route: `billedAmount` is Oxy's fee, not the cost of the tokens. */
267
+ readonly platformFeeOnly: boolean;
268
+ readonly settledAt: string;
265
269
  }
266
270
 
267
271
  /** The endpoint returns either a customer receipt or internal technical usage. */
@@ -277,33 +281,33 @@ export type OxyGenerationRecord = OxyGenerationReceipt | MeteredGeneration;
277
281
  * compute one.
278
282
  */
279
283
  export class OxyInferenceError extends Error {
280
- readonly code: InferenceErrorCode;
281
- readonly retryable: boolean;
282
- readonly requestId: string;
283
- readonly status: number;
284
- /** How long to wait. Only ever present when `retryable`. */
285
- readonly retryAfterMs?: number;
286
- /** The request field at fault, for `invalid_request`. */
287
- readonly param?: string;
288
-
289
- constructor(input: {
290
- code: InferenceErrorCode;
291
- message: string;
292
- retryable: boolean;
293
- requestId: string;
294
- status: number;
295
- retryAfterMs?: number;
296
- param?: string;
297
- }) {
298
- super(input.message);
299
- this.name = 'OxyInferenceError';
300
- this.code = input.code;
301
- this.retryable = input.retryable;
302
- this.requestId = input.requestId;
303
- this.status = input.status;
304
- if (input.retryAfterMs !== undefined) this.retryAfterMs = input.retryAfterMs;
305
- if (input.param !== undefined) this.param = input.param;
306
- }
284
+ readonly code: InferenceErrorCode;
285
+ readonly retryable: boolean;
286
+ readonly requestId: string;
287
+ readonly status: number;
288
+ /** How long to wait. Only ever present when `retryable`. */
289
+ readonly retryAfterMs?: number;
290
+ /** The request field at fault, for `invalid_request`. */
291
+ readonly param?: string;
292
+
293
+ constructor(input: {
294
+ code: InferenceErrorCode;
295
+ message: string;
296
+ retryable: boolean;
297
+ requestId: string;
298
+ status: number;
299
+ retryAfterMs?: number;
300
+ param?: string;
301
+ }) {
302
+ super(input.message);
303
+ this.name = 'OxyInferenceError';
304
+ this.code = input.code;
305
+ this.retryable = input.retryable;
306
+ this.requestId = input.requestId;
307
+ this.status = input.status;
308
+ if (input.retryAfterMs !== undefined) this.retryAfterMs = input.retryAfterMs;
309
+ if (input.param !== undefined) this.param = input.param;
310
+ }
307
311
  }
308
312
 
309
313
  /**
@@ -315,31 +319,31 @@ export class OxyInferenceError extends Error {
315
319
  * safely interpret a nominally successful stream.
316
320
  */
317
321
  export class OxyInferenceProtocolError extends Error {
318
- /** The request id from the response header, when the edge supplied one. */
319
- readonly requestId?: string;
320
-
321
- constructor(message: string, requestId?: string) {
322
- super(message);
323
- this.name = 'OxyInferenceProtocolError';
324
- if (requestId !== undefined) this.requestId = requestId;
325
- }
322
+ /** The request id from the response header, when the edge supplied one. */
323
+ readonly requestId?: string;
324
+
325
+ constructor(message: string, requestId?: string) {
326
+ super(message);
327
+ this.name = 'OxyInferenceProtocolError';
328
+ if (requestId !== undefined) this.requestId = requestId;
329
+ }
326
330
  }
327
331
 
328
332
  /** `{ data, count }` — the catalogue's collection envelope. */
329
333
  interface CatalogueCollection<T> {
330
- data: T[];
331
- count: number;
334
+ data: T[];
335
+ count: number;
332
336
  }
333
337
 
334
338
  /** The shape `/v1/responses` and `/v1/generations/:id` return on a refusal. */
335
339
  interface WireInferenceError {
336
- schemaVersion?: number;
337
- code?: string;
338
- message?: string;
339
- retryable?: boolean;
340
- requestId?: string;
341
- retryAfterMs?: number;
342
- param?: string;
340
+ schemaVersion?: number;
341
+ code?: string;
342
+ message?: string;
343
+ retryable?: boolean;
344
+ requestId?: string;
345
+ retryAfterMs?: number;
346
+ param?: string;
343
347
  }
344
348
 
345
349
  /**
@@ -358,469 +362,469 @@ interface WireInferenceError {
358
362
  * and an unreadable failure must still reach the caller as one.
359
363
  */
360
364
  export class OxyInferenceClient {
361
- readonly #baseURL: string;
362
- readonly #credential: OxyInferenceCredential;
363
- readonly #fetch: OxyInferenceFetch;
364
-
365
- constructor(options: OxyInferenceClientOptions) {
366
- const baseURL = options.baseURL ?? OXY_INFERENCE_BASE_URL;
367
- this.#baseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
368
- this.#credential = options.credential;
369
-
370
- const fetchImpl = options.fetch ?? globalThis.fetch;
371
- if (fetchImpl === undefined) {
372
- throw new Error(
373
- 'OxyInferenceClient needs a fetch implementation: this runtime has no global fetch, so pass one as `fetch`.',
374
- );
375
- }
376
- this.#fetch = fetchImpl;
365
+ readonly #baseURL: string;
366
+ readonly #credential: OxyInferenceCredential;
367
+ readonly #fetch: OxyInferenceFetch;
368
+
369
+ constructor(options: OxyInferenceClientOptions) {
370
+ const baseURL = options.baseURL ?? OXY_INFERENCE_BASE_URL;
371
+ this.#baseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
372
+ this.#credential = options.credential;
373
+
374
+ const fetchImpl = options.fetch ?? globalThis.fetch;
375
+ if (fetchImpl === undefined) {
376
+ throw new Error(
377
+ 'OxyInferenceClient needs a fetch implementation: this runtime has no global fetch, so pass one as `fetch`.',
378
+ );
377
379
  }
378
-
379
- /**
380
- * The models this caller may use — `GET /v1/models`.
381
- *
382
- * Audience-scoped server-side. A machine credential and an anonymous caller
383
- * both see the PUBLIC catalogue; only an internal/system application's
384
- * service token sees internal-only routes.
385
- *
386
- * **`[]` is a normal answer**: a route is not publicly exposed until
387
- * somebody has reviewed the right to resell it. An official application's
388
- * credential also sees the internal catalogue Oxy keeps in step with
389
- * Kaana automatically.
390
- *
391
- * Each entry's `capabilities.reasoningEfforts` lists the efforts a request
392
- * may name in `reasoning.effort` (empty: none), and `releasedAt`, when
393
- * present, is the date the upstream provider published the model.
394
- */
395
- async listModels(options: { signal?: AbortSignal } = {}): Promise<ModelCatalogueEntry[]> {
396
- const body = await this.#request<CatalogueCollection<ModelCatalogueEntry>>(
397
- 'GET',
398
- '/v1/models',
399
- { ...(options.signal === undefined ? {} : { signal: options.signal }) },
400
- );
401
- return body.data;
380
+ this.#fetch = fetchImpl;
381
+ }
382
+
383
+ /**
384
+ * The models this caller may use — `GET /v1/models`.
385
+ *
386
+ * Audience-scoped server-side. A machine credential and an anonymous caller
387
+ * both see the PUBLIC catalogue; only an internal/system application's
388
+ * service token sees internal-only routes.
389
+ *
390
+ * **`[]` is a normal answer**: a route is not publicly exposed until
391
+ * somebody has reviewed the right to resell it. An official application's
392
+ * credential also sees the internal catalogue Oxy keeps in step with
393
+ * Kaana automatically.
394
+ *
395
+ * Each entry's `capabilities.reasoningEfforts` lists the efforts a request
396
+ * may name in `reasoning.effort` (empty: none), and `releasedAt`, when
397
+ * present, is the date the upstream provider published the model.
398
+ */
399
+ async listModels(options: { signal?: AbortSignal } = {}): Promise<ModelCatalogueEntry[]> {
400
+ const body = await this.#request<CatalogueCollection<ModelCatalogueEntry>>(
401
+ 'GET',
402
+ '/v1/models',
403
+ { ...(options.signal === undefined ? {} : { signal: options.signal }) },
404
+ );
405
+ return body.data;
406
+ }
407
+
408
+ /**
409
+ * One catalogue entry by its canonical id — `GET /v1/models/:publisher/:model`.
410
+ *
411
+ * The id is TWO path segments, because a canonical model id contains a slash
412
+ * and a single encoded segment would never match the route.
413
+ *
414
+ * A model you may not see answers 404 identically to one that does not
415
+ * exist, deliberately: the catalogue is never an existence oracle for what
416
+ * Oxy runs internally.
417
+ *
418
+ * @param modelId - `<publisher>/<model>`. A revision pin
419
+ * (`<publisher>/<model>@<revision>`) names a model REFERENCE rather than a
420
+ * model and is rejected here rather than sent, because the catalogue is
421
+ * keyed on models and a pinned reference would 404 indistinguishably from
422
+ * "no such model".
423
+ */
424
+ async getModel(
425
+ modelId: string,
426
+ options: { signal?: AbortSignal } = {},
427
+ ): Promise<ModelCatalogueEntry> {
428
+ const parsed = modelIdSchema.safeParse(modelId);
429
+ if (!parsed.success) {
430
+ throw new Error(
431
+ `Not a canonical model id: ${modelId}. Expected <publisher>/<model>, e.g. acme/some-model.`,
432
+ );
402
433
  }
403
434
 
404
- /**
405
- * One catalogue entry by its canonical id — `GET /v1/models/:publisher/:model`.
406
- *
407
- * The id is TWO path segments, because a canonical model id contains a slash
408
- * and a single encoded segment would never match the route.
409
- *
410
- * A model you may not see answers 404 identically to one that does not
411
- * exist, deliberately: the catalogue is never an existence oracle for what
412
- * Oxy runs internally.
413
- *
414
- * @param modelId - `<publisher>/<model>`. A revision pin
415
- * (`<publisher>/<model>@<revision>`) names a model REFERENCE rather than a
416
- * model and is rejected here rather than sent, because the catalogue is
417
- * keyed on models and a pinned reference would 404 indistinguishably from
418
- * "no such model".
419
- */
420
- async getModel(
421
- modelId: string,
422
- options: { signal?: AbortSignal } = {},
423
- ): Promise<ModelCatalogueEntry> {
424
- const parsed = modelIdSchema.safeParse(modelId);
425
- if (!parsed.success) {
426
- throw new Error(
427
- `Not a canonical model id: ${modelId}. Expected <publisher>/<model>, e.g. acme/some-model.`,
428
- );
429
- }
430
-
431
- const [publisher, model] = parsed.data.split('/');
432
- const body = await this.#request<{ data: ModelCatalogueEntry }>(
433
- 'GET',
434
- `/v1/models/${encodeURIComponent(publisher)}/${encodeURIComponent(model)}`,
435
- { ...(options.signal === undefined ? {} : { signal: options.signal }) },
436
- );
437
- return body.data;
435
+ const [publisher, model] = parsed.data.split('/');
436
+ const body = await this.#request<{ data: ModelCatalogueEntry }>(
437
+ 'GET',
438
+ `/v1/models/${encodeURIComponent(publisher)}/${encodeURIComponent(model)}`,
439
+ { ...(options.signal === undefined ? {} : { signal: options.signal }) },
440
+ );
441
+ return body.data;
442
+ }
443
+
444
+ /**
445
+ * The routing profiles this caller may select — `GET /v1/models/routing-profiles`.
446
+ *
447
+ * A profile is a named strategy for CHOOSING among routes, not a model: no
448
+ * publisher, no revision, no licence, no weights. Like the model list, `[]`
449
+ * is a normal answer.
450
+ */
451
+ async listRoutingProfiles(options: { signal?: AbortSignal } = {}): Promise<RoutingProfile[]> {
452
+ const body = await this.#request<CatalogueCollection<RoutingProfile>>(
453
+ 'GET',
454
+ '/v1/models/routing-profiles',
455
+ { ...(options.signal === undefined ? {} : { signal: options.signal }) },
456
+ );
457
+ return body.data;
458
+ }
459
+
460
+ /**
461
+ * Send one non-streaming inference request — `POST /v1/responses`.
462
+ *
463
+ * @throws {OxyInferenceError} for every refusal, carrying the server's own
464
+ * `code`, `retryable` and `requestId`.
465
+ */
466
+ async respond(
467
+ request: OxyResponsesRequest,
468
+ options: OxyInferenceRequestOptions = {},
469
+ ): Promise<OxyInferenceResponse> {
470
+ return this.#request<OxyInferenceResponse>('POST', '/v1/responses', {
471
+ body: request,
472
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
473
+ ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
474
+ ...(options.delegatedUserId === undefined
475
+ ? {}
476
+ : { delegatedUserId: options.delegatedUserId }),
477
+ });
478
+ }
479
+
480
+ /** Typed nonstreaming decisions. Idempotency conflicts are never replayed or retried. */
481
+ async decide(
482
+ request: DecisionRequest,
483
+ options: OxyInferenceRequestOptions = {},
484
+ ): Promise<DecisionSuccess> {
485
+ const input = decisionRequestSchema.parse(request);
486
+ const payload = await this.#request<unknown>('POST', '/v1/decisions', {
487
+ body: input,
488
+ bindRequestId: true,
489
+ signal: options.signal,
490
+ idempotencyKey: options.idempotencyKey,
491
+ delegatedUserId: options.delegatedUserId,
492
+ });
493
+ const parsed = decisionSuccessSchema.safeParse(payload);
494
+ if (
495
+ !parsed.success ||
496
+ parsed.data.model !== input.model ||
497
+ !decisionAnswersMatch(input, parsed.data.data)
498
+ ) {
499
+ throw new OxyInferenceProtocolError('Invalid or mismatched decisions response.');
438
500
  }
439
-
440
- /**
441
- * The routing profiles this caller may select — `GET /v1/models/routing-profiles`.
442
- *
443
- * A profile is a named strategy for CHOOSING among routes, not a model: no
444
- * publisher, no revision, no licence, no weights. Like the model list, `[]`
445
- * is a normal answer.
446
- */
447
- async listRoutingProfiles(
448
- options: { signal?: AbortSignal } = {},
449
- ): Promise<RoutingProfile[]> {
450
- const body = await this.#request<CatalogueCollection<RoutingProfile>>(
451
- 'GET',
452
- '/v1/models/routing-profiles',
453
- { ...(options.signal === undefined ? {} : { signal: options.signal }) },
454
- );
455
- return body.data;
501
+ return parsed.data;
502
+ }
503
+
504
+ /** Generate audio bytes through Oxy's authenticated inference edge. */
505
+ async speech(
506
+ request: OxySpeechRequest,
507
+ options: OxyInferenceRequestOptions = {},
508
+ ): Promise<OxySpeechResponse> {
509
+ const response = await this.#fetch(`${this.#baseURL}/v1/audio/speech`, {
510
+ method: 'POST',
511
+ headers: await this.#headers('audio/*', {
512
+ hasBody: true,
513
+ ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
514
+ ...(options.delegatedUserId === undefined
515
+ ? {}
516
+ : { delegatedUserId: options.delegatedUserId }),
517
+ }),
518
+ body: JSON.stringify(request),
519
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
520
+ });
521
+ const requestId = response.headers.get('X-Oxy-Request-Id') ?? '';
522
+ if (!response.ok) {
523
+ throw toInferenceError(
524
+ await response.json().catch(() => undefined),
525
+ response.status,
526
+ requestId,
527
+ );
456
528
  }
457
-
458
- /**
459
- * Send one non-streaming inference request — `POST /v1/responses`.
460
- *
461
- * @throws {OxyInferenceError} for every refusal, carrying the server's own
462
- * `code`, `retryable` and `requestId`.
463
- */
464
- async respond(
465
- request: OxyResponsesRequest,
466
- options: OxyInferenceRequestOptions = {},
467
- ): Promise<OxyInferenceResponse> {
468
- return this.#request<OxyInferenceResponse>('POST', '/v1/responses', {
469
- body: request,
470
- ...(options.signal === undefined ? {} : { signal: options.signal }),
471
- ...(options.idempotencyKey === undefined
472
- ? {}
473
- : { idempotencyKey: options.idempotencyKey }),
474
- ...(options.delegatedUserId === undefined
475
- ? {}
476
- : { delegatedUserId: options.delegatedUserId }),
477
- });
529
+ const mediaType = inferenceAudioMediaTypeSchema.safeParse(mediaTypeOf(response));
530
+ if (!mediaType.success || !response.body) {
531
+ await cancelUnreadResponse(response);
532
+ throw protocolError('The inference API returned no supported audio body.', requestId);
478
533
  }
479
-
480
- /** Typed nonstreaming decisions. Idempotency conflicts are never replayed or retried. */
481
- async decide(
482
- request: DecisionRequest,
483
- options: OxyInferenceRequestOptions = {},
484
- ): Promise<DecisionSuccess> {
485
- const input = decisionRequestSchema.parse(request);
486
- const payload = await this.#request<unknown>('POST', '/v1/decisions', {
487
- body: input,
488
- bindRequestId: true,
489
- signal: options.signal,
490
- idempotencyKey: options.idempotencyKey,
491
- delegatedUserId: options.delegatedUserId,
492
- });
493
- const parsed = decisionSuccessSchema.safeParse(payload);
494
- if (!parsed.success || parsed.data.model !== input.model ||
495
- !decisionAnswersMatch(input, parsed.data.data)) {
496
- throw new OxyInferenceProtocolError('Invalid or mismatched decisions response.');
534
+ const reader = response.body.getReader();
535
+ const chunks: Uint8Array[] = [];
536
+ let size = 0;
537
+ let completed = false;
538
+ try {
539
+ while (true) {
540
+ const next = await reader.read();
541
+ if (next.done) {
542
+ completed = true;
543
+ break;
497
544
  }
498
- return parsed.data;
545
+ size += next.value.byteLength;
546
+ if (size > MAX_INFERENCE_AUDIO_BYTES)
547
+ throw protocolError('The inference audio exceeds the response limit.', requestId);
548
+ chunks.push(next.value);
549
+ }
550
+ } finally {
551
+ if (!completed) await reader.cancel().catch(() => undefined);
552
+ reader.releaseLock();
499
553
  }
500
-
501
- /** Generate audio bytes through Oxy's authenticated inference edge. */
502
- async speech(
503
- request: OxySpeechRequest,
504
- options: OxyInferenceRequestOptions = {},
505
- ): Promise<OxySpeechResponse> {
506
- const response = await this.#fetch(`${this.#baseURL}/v1/audio/speech`, {
507
- method: 'POST',
508
- headers: await this.#headers('audio/*', {
509
- hasBody: true,
510
- ...(options.idempotencyKey === undefined
511
- ? {}
512
- : { idempotencyKey: options.idempotencyKey }),
513
- ...(options.delegatedUserId === undefined
514
- ? {}
515
- : { delegatedUserId: options.delegatedUserId }),
516
- }),
517
- body: JSON.stringify(request),
518
- ...(options.signal === undefined ? {} : { signal: options.signal }),
519
- });
520
- const requestId = response.headers.get('X-Oxy-Request-Id') ?? '';
521
- if (!response.ok) {
522
- throw toInferenceError(await response.json().catch(() => undefined), response.status, requestId);
523
- }
524
- const mediaType = inferenceAudioMediaTypeSchema.safeParse(mediaTypeOf(response));
525
- if (!mediaType.success || !response.body) {
526
- await cancelUnreadResponse(response);
527
- throw protocolError('The inference API returned no supported audio body.', requestId);
528
- }
529
- const reader = response.body.getReader();
530
- const chunks: Uint8Array[] = [];
531
- let size = 0;
532
- let completed = false;
533
- try {
534
- while (true) {
535
- const next = await reader.read();
536
- if (next.done) { completed = true; break; }
537
- size += next.value.byteLength;
538
- if (size > MAX_INFERENCE_AUDIO_BYTES) throw protocolError('The inference audio exceeds the response limit.', requestId);
539
- chunks.push(next.value);
540
- }
541
- } finally {
542
- if (!completed) await reader.cancel().catch(() => undefined);
543
- reader.releaseLock();
544
- }
545
- if (size === 0) throw protocolError('The inference API returned empty audio.', requestId);
546
- const audio = new Uint8Array(size);
547
- let offset = 0;
548
- for (const chunk of chunks) { audio.set(chunk, offset); offset += chunk.byteLength; }
549
- return { audio, mediaType: mediaType.data, requestId };
554
+ if (size === 0) throw protocolError('The inference API returned empty audio.', requestId);
555
+ const audio = new Uint8Array(size);
556
+ let offset = 0;
557
+ for (const chunk of chunks) {
558
+ audio.set(chunk, offset);
559
+ offset += chunk.byteLength;
550
560
  }
561
+ return { audio, mediaType: mediaType.data, requestId };
562
+ }
563
+
564
+ /**
565
+ * Stream one inference request from `POST /v1/responses`.
566
+ *
567
+ * The transport choice is method-level: this method adds `stream: true` and
568
+ * requests SSE, while {@link respond} never does. Every yielded value has
569
+ * already passed `inferenceStreamEventSchema`, belongs to the request id in
570
+ * the response header and advances the stream sequence. A terminal `error`
571
+ * event is yielded as part of the published union; an HTTP refusal before
572
+ * the stream opens throws {@link OxyInferenceError}.
573
+ *
574
+ * Breaking out of the iterator cancels the response body. Passing an abort
575
+ * signal additionally lets the caller cancel while waiting for the first or
576
+ * any later event.
577
+ *
578
+ * @throws {OxyInferenceError} when the edge refuses before opening SSE.
579
+ * @throws {OxyInferenceProtocolError} when a successful SSE response cannot
580
+ * be interpreted under the published stream contract.
581
+ */
582
+ async *stream(
583
+ request: OxyResponsesRequest,
584
+ options: OxyInferenceRequestOptions = {},
585
+ ): AsyncGenerator<InferenceStreamEvent> {
586
+ const response = await this.#fetch(`${this.#baseURL}/v1/responses`, {
587
+ method: 'POST',
588
+ headers: await this.#headers('text/event-stream', {
589
+ hasBody: true,
590
+ ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
591
+ ...(options.delegatedUserId === undefined
592
+ ? {}
593
+ : { delegatedUserId: options.delegatedUserId }),
594
+ }),
595
+ body: JSON.stringify({ ...request, stream: true }),
596
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
597
+ });
551
598
 
552
- /**
553
- * Stream one inference request from `POST /v1/responses`.
554
- *
555
- * The transport choice is method-level: this method adds `stream: true` and
556
- * requests SSE, while {@link respond} never does. Every yielded value has
557
- * already passed `inferenceStreamEventSchema`, belongs to the request id in
558
- * the response header and advances the stream sequence. A terminal `error`
559
- * event is yielded as part of the published union; an HTTP refusal before
560
- * the stream opens throws {@link OxyInferenceError}.
561
- *
562
- * Breaking out of the iterator cancels the response body. Passing an abort
563
- * signal additionally lets the caller cancel while waiting for the first or
564
- * any later event.
565
- *
566
- * @throws {OxyInferenceError} when the edge refuses before opening SSE.
567
- * @throws {OxyInferenceProtocolError} when a successful SSE response cannot
568
- * be interpreted under the published stream contract.
569
- */
570
- async *stream(
571
- request: OxyResponsesRequest,
572
- options: OxyInferenceRequestOptions = {},
573
- ): AsyncGenerator<InferenceStreamEvent> {
574
- const response = await this.#fetch(`${this.#baseURL}/v1/responses`, {
575
- method: 'POST',
576
- headers: await this.#headers('text/event-stream', {
577
- hasBody: true,
578
- ...(options.idempotencyKey === undefined
579
- ? {}
580
- : { idempotencyKey: options.idempotencyKey }),
581
- ...(options.delegatedUserId === undefined
582
- ? {}
583
- : { delegatedUserId: options.delegatedUserId }),
584
- }),
585
- body: JSON.stringify({ ...request, stream: true }),
586
- ...(options.signal === undefined ? {} : { signal: options.signal }),
587
- });
588
-
589
- if (!response.ok) {
590
- const payload: unknown = await response.json().catch(() => undefined);
591
- throw toInferenceError(
592
- payload,
593
- response.status,
594
- response.headers.get('X-Oxy-Request-Id'),
595
- );
596
- }
597
-
598
- const requestId = response.headers.get('X-Oxy-Request-Id') ?? undefined;
599
- const mediaType = mediaTypeOf(response);
600
- if (mediaType !== 'text/event-stream') {
601
- await cancelUnreadResponse(response);
602
- throw protocolError(
603
- `The inference API answered a streaming request with ${mediaType ?? 'no Content-Type'} instead of text/event-stream.`,
604
- requestId,
605
- );
606
- }
607
- if (requestId === undefined || requestId.length === 0) {
608
- await cancelUnreadResponse(response);
609
- throw protocolError(
610
- 'The inference API opened a stream without X-Oxy-Request-Id.',
611
- requestId,
612
- );
613
- }
614
- if (response.body === null) {
615
- throw protocolError(
616
- 'The inference API opened a stream with no response body.',
617
- requestId,
618
- );
619
- }
620
-
621
- let lastSequence = -1;
622
- let terminal = false;
623
- for await (const frame of decodeEventStream(response.body, requestId)) {
624
- if (terminal) {
625
- throw protocolError(
626
- 'The inference API sent an event after a terminal event.',
627
- requestId,
628
- );
629
- }
630
-
631
- let payload: unknown;
632
- try {
633
- payload = JSON.parse(frame.data);
634
- } catch (_error) {
635
- throw protocolError(
636
- `The inference API sent a ${frame.name || 'unnamed'} SSE event that is not JSON.`,
637
- requestId,
638
- );
639
- }
640
-
641
- const parsed = inferenceStreamEventSchema.safeParse(payload);
642
- if (!parsed.success) {
643
- const path = parsed.error.issues[0]?.path;
644
- throw protocolError(
645
- `The inference API sent an event outside the published stream contract at ${path === undefined || path.length === 0 ? 'an unknown field' : path.join('.')}.`,
646
- requestId,
647
- );
648
- }
649
-
650
- const event = parsed.data;
651
- if (frame.name !== event.type) {
652
- throw protocolError(
653
- `The inference API framed a ${event.type} event as ${frame.name || 'unnamed'}.`,
654
- requestId,
655
- );
656
- }
657
- if (event.requestId !== requestId) {
658
- throw protocolError(
659
- 'The inference API sent an event for a different request id.',
660
- requestId,
661
- );
662
- }
663
- if (event.sequence <= lastSequence) {
664
- throw protocolError(
665
- 'The inference API sent a stream sequence that did not advance.',
666
- requestId,
667
- );
668
- }
669
-
670
- lastSequence = event.sequence;
671
- terminal = event.type === 'done' || event.type === 'error';
672
- yield event;
673
- }
674
-
675
- if (!terminal) {
676
- throw protocolError(
677
- 'The inference API closed the stream before a terminal done or error event.',
678
- requestId,
679
- );
680
- }
599
+ if (!response.ok) {
600
+ const payload: unknown = await response.json().catch(() => undefined);
601
+ throw toInferenceError(payload, response.status, response.headers.get('X-Oxy-Request-Id'));
681
602
  }
682
603
 
683
- /**
684
- * Read back the settled receipt for one request —
685
- * `GET /v1/generations/:id`.
686
- *
687
- * `id` is the `requestId` you already hold (it is on every response and
688
- * every error) or the `generationId`. Requires the `inference:usage:read`
689
- * scope; a caller without it, or one whose application did not make the
690
- * request, is told the receipt does not exist rather than that it belongs to
691
- * somebody else.
692
- */
693
- async getGeneration(
694
- id: string,
695
- options: { signal?: AbortSignal; delegatedUserId?: string } = {},
696
- ): Promise<OxyGenerationReceipt> {
697
- const record = await this.getGenerationRecord(id, options);
698
- if (record.schemaVersion !== 1) {
699
- throw new OxyInferenceProtocolError('Internal usage has no financial receipt. Use getGenerationRecord().', record.requestId);
700
- }
701
- return record;
604
+ const requestId = response.headers.get('X-Oxy-Request-Id') ?? undefined;
605
+ const mediaType = mediaTypeOf(response);
606
+ if (mediaType !== 'text/event-stream') {
607
+ await cancelUnreadResponse(response);
608
+ throw protocolError(
609
+ `The inference API answered a streaming request with ${mediaType ?? 'no Content-Type'} instead of text/event-stream.`,
610
+ requestId,
611
+ );
612
+ }
613
+ if (requestId === undefined || requestId.length === 0) {
614
+ await cancelUnreadResponse(response);
615
+ throw protocolError('The inference API opened a stream without X-Oxy-Request-Id.', requestId);
616
+ }
617
+ if (response.body === null) {
618
+ throw protocolError('The inference API opened a stream with no response body.', requestId);
702
619
  }
703
620
 
704
- /** Read a financial receipt (v1) or an internal technical record (v2). */
705
- async getGenerationRecord(
706
- id: string,
707
- options: { signal?: AbortSignal; delegatedUserId?: string } = {},
708
- ): Promise<OxyGenerationRecord> {
709
- const body = await this.#request<{ data: OxyGenerationRecord }>(
710
- 'GET', `/v1/generations/${encodeURIComponent(id)}`, options,
621
+ let lastSequence = -1;
622
+ let terminal = false;
623
+ for await (const frame of decodeEventStream(response.body, requestId)) {
624
+ if (terminal) {
625
+ throw protocolError('The inference API sent an event after a terminal event.', requestId);
626
+ }
627
+
628
+ let payload: unknown;
629
+ try {
630
+ payload = JSON.parse(frame.data);
631
+ } catch (_error) {
632
+ throw protocolError(
633
+ `The inference API sent a ${frame.name || 'unnamed'} SSE event that is not JSON.`,
634
+ requestId,
711
635
  );
712
- return body.data;
713
- }
636
+ }
637
+
638
+ const parsed = inferenceStreamEventSchema.safeParse(payload);
639
+ if (!parsed.success) {
640
+ const path = parsed.error.issues[0]?.path;
641
+ throw protocolError(
642
+ `The inference API sent an event outside the published stream contract at ${path === undefined || path.length === 0 ? 'an unknown field' : path.join('.')}.`,
643
+ requestId,
644
+ );
645
+ }
714
646
 
715
- /**
716
- * Recover the existing record using the original credential and key. This
717
- * performs only GET; 404 is unknown/pending, never proof of zero cost and
718
- * never an instruction to repeat decide(). Lost answers are not retained.
719
- */
720
- async getGenerationRecordByIdempotencyKey(
721
- key: string,
722
- options: { signal?: AbortSignal; delegatedUserId?: string } = {},
723
- ): Promise<OxyGenerationRecord> {
724
- if (key.length === 0 || key.length > 128) throw new Error('Invalid original idempotency key.');
725
- const body = await this.#request<{ data: OxyGenerationRecord }>(
726
- 'GET', '/v1/generations/by-idempotency-key', { ...options, idempotencyKey: key },
647
+ const event = parsed.data;
648
+ if (frame.name !== event.type) {
649
+ throw protocolError(
650
+ `The inference API framed a ${event.type} event as ${frame.name || 'unnamed'}.`,
651
+ requestId,
727
652
  );
728
- return body.data;
729
- }
653
+ }
654
+ if (event.requestId !== requestId) {
655
+ throw protocolError(
656
+ 'The inference API sent an event for a different request id.',
657
+ requestId,
658
+ );
659
+ }
660
+ if (event.sequence <= lastSequence) {
661
+ throw protocolError(
662
+ 'The inference API sent a stream sequence that did not advance.',
663
+ requestId,
664
+ );
665
+ }
730
666
 
731
- /** The bearer for this request, from whichever lane was configured. */
732
- async #bearer(): Promise<string> {
733
- const value =
734
- typeof this.#credential === 'string'
735
- ? this.#credential
736
- : await this.#credential();
737
- if (value === null || value === undefined || value.length === 0) {
738
- throw new Error(
739
- 'OxyInferenceClient has no bearer: the configured credential resolved to nothing. On the Oxy auth lane this usually means the session is not restored yet.',
740
- );
741
- }
742
- return value;
667
+ lastSequence = event.sequence;
668
+ terminal = event.type === 'done' || event.type === 'error';
669
+ yield event;
743
670
  }
744
671
 
745
- /** Build the authenticated headers for one request, re-reading its bearer. */
746
- async #headers(
747
- accept: 'application/json' | 'text/event-stream' | 'audio/*',
748
- options: {
749
- hasBody: boolean;
750
- idempotencyKey?: string;
751
- delegatedUserId?: string;
752
- },
753
- ): Promise<Record<string, string>> {
754
- const headers: Record<string, string> = {
755
- Authorization: `Bearer ${await this.#bearer()}`,
756
- Accept: accept,
757
- };
758
- if (options.hasBody) headers['Content-Type'] = 'application/json';
759
- if (options.idempotencyKey !== undefined) {
760
- headers['Idempotency-Key'] = options.idempotencyKey;
761
- }
762
- if (options.delegatedUserId !== undefined) {
763
- headers['X-Oxy-User-Id'] = options.delegatedUserId;
764
- }
765
- return headers;
672
+ if (!terminal) {
673
+ throw protocolError(
674
+ 'The inference API closed the stream before a terminal done or error event.',
675
+ requestId,
676
+ );
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Read back the settled receipt for one request —
682
+ * `GET /v1/generations/:id`.
683
+ *
684
+ * `id` is the `requestId` you already hold (it is on every response and
685
+ * every error) or the `generationId`. Requires the `inference:usage:read`
686
+ * scope; a caller without it, or one whose application did not make the
687
+ * request, is told the receipt does not exist rather than that it belongs to
688
+ * somebody else.
689
+ */
690
+ async getGeneration(
691
+ id: string,
692
+ options: { signal?: AbortSignal; delegatedUserId?: string } = {},
693
+ ): Promise<OxyGenerationReceipt> {
694
+ const record = await this.getGenerationRecord(id, options);
695
+ if (record.schemaVersion !== 1) {
696
+ throw new OxyInferenceProtocolError(
697
+ 'Internal usage has no financial receipt. Use getGenerationRecord().',
698
+ record.requestId,
699
+ );
766
700
  }
701
+ return record;
702
+ }
703
+
704
+ /** Read a financial receipt (v1) or an internal technical record (v2). */
705
+ async getGenerationRecord(
706
+ id: string,
707
+ options: { signal?: AbortSignal; delegatedUserId?: string } = {},
708
+ ): Promise<OxyGenerationRecord> {
709
+ const body = await this.#request<{ data: OxyGenerationRecord }>(
710
+ 'GET',
711
+ `/v1/generations/${encodeURIComponent(id)}`,
712
+ options,
713
+ );
714
+ return body.data;
715
+ }
716
+
717
+ /**
718
+ * Recover the existing record using the original credential and key. This
719
+ * performs only GET; 404 is unknown/pending, never proof of zero cost and
720
+ * never an instruction to repeat decide(). Lost answers are not retained.
721
+ */
722
+ async getGenerationRecordByIdempotencyKey(
723
+ key: string,
724
+ options: { signal?: AbortSignal; delegatedUserId?: string } = {},
725
+ ): Promise<OxyGenerationRecord> {
726
+ if (key.length === 0 || key.length > 128) throw new Error('Invalid original idempotency key.');
727
+ const body = await this.#request<{ data: OxyGenerationRecord }>(
728
+ 'GET',
729
+ '/v1/generations/by-idempotency-key',
730
+ { ...options, idempotencyKey: key },
731
+ );
732
+ return body.data;
733
+ }
734
+
735
+ /** The bearer for this request, from whichever lane was configured. */
736
+ async #bearer(): Promise<string> {
737
+ const value =
738
+ typeof this.#credential === 'string' ? this.#credential : await this.#credential();
739
+ if (value === null || value === undefined || value.length === 0) {
740
+ throw new Error(
741
+ 'OxyInferenceClient has no bearer: the configured credential resolved to nothing. On the Oxy auth lane this usually means the session is not restored yet.',
742
+ );
743
+ }
744
+ return value;
745
+ }
746
+
747
+ /** Build the authenticated headers for one request, re-reading its bearer. */
748
+ async #headers(
749
+ accept: 'application/json' | 'text/event-stream' | 'audio/*',
750
+ options: {
751
+ hasBody: boolean;
752
+ idempotencyKey?: string;
753
+ delegatedUserId?: string;
754
+ },
755
+ ): Promise<Record<string, string>> {
756
+ const headers: Record<string, string> = {
757
+ Authorization: `Bearer ${await this.#bearer()}`,
758
+ Accept: accept,
759
+ };
760
+ if (options.hasBody) headers['Content-Type'] = 'application/json';
761
+ if (options.idempotencyKey !== undefined) {
762
+ headers['Idempotency-Key'] = options.idempotencyKey;
763
+ }
764
+ if (options.delegatedUserId !== undefined) {
765
+ headers['X-Oxy-User-Id'] = options.delegatedUserId;
766
+ }
767
+ return headers;
768
+ }
769
+
770
+ /**
771
+ * One request, and the one place a refusal becomes an
772
+ * {@link OxyInferenceError}.
773
+ *
774
+ * Two error shapes arrive here, because two routers serve `/v1`. The edge
775
+ * returns the contract error at the top level; the catalogue returns the
776
+ * platform's `{ error, message }` envelope. Both are read, and a body that
777
+ * is neither still produces an `OxyInferenceError` — with the code the
778
+ * status maps to — rather than a bare `Error`, so a caller's `catch` never
779
+ * has to branch on which router answered.
780
+ */
781
+ async #request<T>(
782
+ method: 'GET' | 'POST',
783
+ path: string,
784
+ options: {
785
+ body?: unknown;
786
+ bindRequestId?: boolean;
787
+ signal?: AbortSignal;
788
+ idempotencyKey?: string;
789
+ delegatedUserId?: string;
790
+ },
791
+ ): Promise<T> {
792
+ const response = await this.#fetch(`${this.#baseURL}${path}`, {
793
+ method,
794
+ headers: await this.#headers('application/json', {
795
+ hasBody: options.body !== undefined,
796
+ ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
797
+ ...(options.delegatedUserId === undefined
798
+ ? {}
799
+ : { delegatedUserId: options.delegatedUserId }),
800
+ }),
801
+ ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
802
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
803
+ });
767
804
 
768
- /**
769
- * One request, and the one place a refusal becomes an
770
- * {@link OxyInferenceError}.
771
- *
772
- * Two error shapes arrive here, because two routers serve `/v1`. The edge
773
- * returns the contract error at the top level; the catalogue returns the
774
- * platform's `{ error, message }` envelope. Both are read, and a body that
775
- * is neither still produces an `OxyInferenceError` — with the code the
776
- * status maps to — rather than a bare `Error`, so a caller's `catch` never
777
- * has to branch on which router answered.
778
- */
779
- async #request<T>(
780
- method: 'GET' | 'POST',
781
- path: string,
782
- options: {
783
- body?: unknown;
784
- bindRequestId?: boolean;
785
- signal?: AbortSignal;
786
- idempotencyKey?: string;
787
- delegatedUserId?: string;
788
- },
789
- ): Promise<T> {
790
- const response = await this.#fetch(`${this.#baseURL}${path}`, {
791
- method,
792
- headers: await this.#headers('application/json', {
793
- hasBody: options.body !== undefined,
794
- ...(options.idempotencyKey === undefined
795
- ? {}
796
- : { idempotencyKey: options.idempotencyKey }),
797
- ...(options.delegatedUserId === undefined
798
- ? {}
799
- : { delegatedUserId: options.delegatedUserId }),
800
- }),
801
- ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
802
- ...(options.signal === undefined ? {} : { signal: options.signal }),
803
- });
804
-
805
- const payload: unknown = await response.json().catch(() => undefined);
806
-
807
- if (!response.ok) {
808
- throw toInferenceError(
809
- payload,
810
- response.status,
811
- response.headers.get('X-Oxy-Request-Id'),
812
- );
813
- }
805
+ const payload: unknown = await response.json().catch(() => undefined);
814
806
 
815
- if (options.bindRequestId) {
816
- const requestId = response.headers.get('X-Oxy-Request-Id');
817
- if (!requestId || typeof payload !== 'object' || payload === null ||
818
- !('requestId' in payload) || payload.requestId !== requestId) {
819
- throw new OxyInferenceProtocolError('Decisions response request ID does not match its header.', requestId ?? undefined);
820
- }
821
- }
822
- return payload as T;
807
+ if (!response.ok) {
808
+ throw toInferenceError(payload, response.status, response.headers.get('X-Oxy-Request-Id'));
809
+ }
810
+
811
+ if (options.bindRequestId) {
812
+ const requestId = response.headers.get('X-Oxy-Request-Id');
813
+ if (
814
+ !requestId ||
815
+ typeof payload !== 'object' ||
816
+ payload === null ||
817
+ !('requestId' in payload) ||
818
+ payload.requestId !== requestId
819
+ ) {
820
+ throw new OxyInferenceProtocolError(
821
+ 'Decisions response request ID does not match its header.',
822
+ requestId ?? undefined,
823
+ );
824
+ }
823
825
  }
826
+ return payload as T;
827
+ }
824
828
  }
825
829
 
826
830
  /** Bound one public SSE event before parsing it as JSON. */
@@ -831,22 +835,22 @@ const MAX_INFERENCE_STREAM_EVENT_DATA_LINES = 4096;
831
835
 
832
836
  /** One SSE frame after transport decoding and before contract validation. */
833
837
  interface RawInferenceStreamFrame {
834
- readonly name: string;
835
- readonly data: string;
838
+ readonly name: string;
839
+ readonly data: string;
836
840
  }
837
841
 
838
842
  function protocolError(message: string, requestId?: string): OxyInferenceProtocolError {
839
- return new OxyInferenceProtocolError(message, requestId);
843
+ return new OxyInferenceProtocolError(message, requestId);
840
844
  }
841
845
 
842
846
  /** The bare, lowercased media type of a response's `Content-Type`, if any. */
843
847
  function mediaTypeOf(response: Response): string | undefined {
844
- return response.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase();
848
+ return response.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase();
845
849
  }
846
850
 
847
851
  /** Cancel a nominally successful response whose stream contract is unreadable. */
848
852
  async function cancelUnreadResponse(response: Response): Promise<void> {
849
- await response.body?.cancel().catch(() => undefined);
853
+ await response.body?.cancel().catch(() => undefined);
850
854
  }
851
855
 
852
856
  /**
@@ -859,111 +863,111 @@ async function cancelUnreadResponse(response: Response): Promise<void> {
859
863
  * instead of leaving generation running upstream.
860
864
  */
861
865
  async function* decodeEventStream(
862
- body: ReadableStream<Uint8Array>,
863
- requestId: string,
866
+ body: ReadableStream<Uint8Array>,
867
+ requestId: string,
864
868
  ): AsyncGenerator<RawInferenceStreamFrame> {
865
- const reader = body.getReader();
866
- const decoder = new TextDecoder('utf-8', { fatal: true });
867
- let pending = '';
868
- let name = '';
869
- let data: string[] = [];
870
- let accumulated = 0;
871
- let dataLines = 0;
872
- let reachedEnd = false;
873
-
874
- const decode = (chunk?: Uint8Array, options?: TextDecodeOptions): string => {
875
- try {
876
- return decoder.decode(chunk, options);
877
- } catch (_error) {
878
- throw protocolError(
879
- 'The inference API sent an SSE stream that is not valid UTF-8.',
880
- requestId,
881
- );
882
- }
883
- };
884
-
885
- const dispatch = (): RawInferenceStreamFrame | undefined => {
886
- if (data.length === 0 && name.length === 0) return undefined;
887
- const frame = { name, data: data.join('\n') };
888
- name = '';
889
- data = [];
890
- accumulated = 0;
891
- dataLines = 0;
892
- return frame;
893
- };
894
-
895
- const consume = (raw: string): RawInferenceStreamFrame | undefined => {
896
- const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
897
- if (line.length === 0) return dispatch();
898
- if (line.startsWith(':')) return undefined;
899
-
900
- const separator = line.indexOf(':');
901
- const field = separator === -1 ? line : line.slice(0, separator);
902
- const rawValue = separator === -1 ? '' : line.slice(separator + 1);
903
- const value = rawValue.startsWith(' ') ? rawValue.slice(1) : rawValue;
904
-
905
- if (field === 'data') {
906
- dataLines += 1;
907
- if (dataLines > MAX_INFERENCE_STREAM_EVENT_DATA_LINES) {
908
- throw protocolError(
909
- `The inference API sent an SSE event with over ${MAX_INFERENCE_STREAM_EVENT_DATA_LINES} data lines.`,
910
- requestId,
911
- );
912
- }
913
- accumulated += value.length;
914
- if (accumulated > MAX_INFERENCE_STREAM_EVENT_CHARACTERS) {
915
- throw protocolError(
916
- `The inference API sent an SSE event over ${MAX_INFERENCE_STREAM_EVENT_CHARACTERS} characters.`,
917
- requestId,
918
- );
919
- }
920
- data.push(value);
921
- } else if (field === 'event') {
922
- name = value.trim();
923
- }
924
- return undefined;
925
- };
926
-
869
+ const reader = body.getReader();
870
+ const decoder = new TextDecoder('utf-8', { fatal: true });
871
+ let pending = '';
872
+ let name = '';
873
+ let data: string[] = [];
874
+ let accumulated = 0;
875
+ let dataLines = 0;
876
+ let reachedEnd = false;
877
+
878
+ const decode = (chunk?: Uint8Array, options?: TextDecodeOptions): string => {
927
879
  try {
928
- for (;;) {
929
- const chunk = await reader.read();
930
- if (chunk.done) {
931
- reachedEnd = true;
932
- break;
933
- }
934
-
935
- pending += decode(chunk.value, { stream: true });
936
- if (pending.length > MAX_INFERENCE_STREAM_EVENT_CHARACTERS) {
937
- throw protocolError(
938
- `The inference API sent an SSE line over ${MAX_INFERENCE_STREAM_EVENT_CHARACTERS} characters with no boundary.`,
939
- requestId,
940
- );
941
- }
942
-
943
- let newline = pending.indexOf('\n');
944
- while (newline !== -1) {
945
- const frame = consume(pending.slice(0, newline));
946
- pending = pending.slice(newline + 1);
947
- if (frame !== undefined) yield frame;
948
- newline = pending.indexOf('\n');
949
- }
950
- }
880
+ return decoder.decode(chunk, options);
881
+ } catch (_error) {
882
+ throw protocolError(
883
+ 'The inference API sent an SSE stream that is not valid UTF-8.',
884
+ requestId,
885
+ );
886
+ }
887
+ };
888
+
889
+ const dispatch = (): RawInferenceStreamFrame | undefined => {
890
+ if (data.length === 0 && name.length === 0) return undefined;
891
+ const frame = { name, data: data.join('\n') };
892
+ name = '';
893
+ data = [];
894
+ accumulated = 0;
895
+ dataLines = 0;
896
+ return frame;
897
+ };
898
+
899
+ const consume = (raw: string): RawInferenceStreamFrame | undefined => {
900
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
901
+ if (line.length === 0) return dispatch();
902
+ if (line.startsWith(':')) return undefined;
903
+
904
+ const separator = line.indexOf(':');
905
+ const field = separator === -1 ? line : line.slice(0, separator);
906
+ const rawValue = separator === -1 ? '' : line.slice(separator + 1);
907
+ const value = rawValue.startsWith(' ') ? rawValue.slice(1) : rawValue;
908
+
909
+ if (field === 'data') {
910
+ dataLines += 1;
911
+ if (dataLines > MAX_INFERENCE_STREAM_EVENT_DATA_LINES) {
912
+ throw protocolError(
913
+ `The inference API sent an SSE event with over ${MAX_INFERENCE_STREAM_EVENT_DATA_LINES} data lines.`,
914
+ requestId,
915
+ );
916
+ }
917
+ accumulated += value.length;
918
+ if (accumulated > MAX_INFERENCE_STREAM_EVENT_CHARACTERS) {
919
+ throw protocolError(
920
+ `The inference API sent an SSE event over ${MAX_INFERENCE_STREAM_EVENT_CHARACTERS} characters.`,
921
+ requestId,
922
+ );
923
+ }
924
+ data.push(value);
925
+ } else if (field === 'event') {
926
+ name = value.trim();
927
+ }
928
+ return undefined;
929
+ };
930
+
931
+ try {
932
+ for (;;) {
933
+ const chunk = await reader.read();
934
+ if (chunk.done) {
935
+ reachedEnd = true;
936
+ break;
937
+ }
938
+
939
+ pending += decode(chunk.value, { stream: true });
940
+ if (pending.length > MAX_INFERENCE_STREAM_EVENT_CHARACTERS) {
941
+ throw protocolError(
942
+ `The inference API sent an SSE line over ${MAX_INFERENCE_STREAM_EVENT_CHARACTERS} characters with no boundary.`,
943
+ requestId,
944
+ );
945
+ }
946
+
947
+ let newline = pending.indexOf('\n');
948
+ while (newline !== -1) {
949
+ const frame = consume(pending.slice(0, newline));
950
+ pending = pending.slice(newline + 1);
951
+ if (frame !== undefined) yield frame;
952
+ newline = pending.indexOf('\n');
953
+ }
954
+ }
951
955
 
952
- pending += decode();
953
- if (pending.length > 0) {
954
- const frame = consume(pending);
955
- if (frame !== undefined) yield frame;
956
- }
957
- const trailing = dispatch();
958
- if (trailing !== undefined) yield trailing;
959
- } finally {
960
- if (!reachedEnd) {
961
- // Preserve the original parse/abort error if transport cancellation
962
- // itself also refuses: cancellation is cleanup, never the diagnosis.
963
- await reader.cancel().catch(() => undefined);
964
- }
965
- reader.releaseLock();
956
+ pending += decode();
957
+ if (pending.length > 0) {
958
+ const frame = consume(pending);
959
+ if (frame !== undefined) yield frame;
966
960
  }
961
+ const trailing = dispatch();
962
+ if (trailing !== undefined) yield trailing;
963
+ } finally {
964
+ if (!reachedEnd) {
965
+ // Preserve the original parse/abort error if transport cancellation
966
+ // itself also refuses: cancellation is cleanup, never the diagnosis.
967
+ await reader.cancel().catch(() => undefined);
968
+ }
969
+ reader.releaseLock();
970
+ }
967
971
  }
968
972
 
969
973
  /**
@@ -975,16 +979,16 @@ async function* decodeEventStream(
975
979
  * how one outage becomes a retry storm.
976
980
  */
977
981
  const STATUS_FALLBACK_CODE: Readonly<Record<number, InferenceErrorCode>> = {
978
- 400: 'invalid_request',
979
- 401: 'authentication_failed',
980
- 403: 'permission_denied',
981
- 404: 'model_not_found',
982
- 409: 'idempotency_conflict',
983
- 413: 'request_too_large',
984
- 429: 'rate_limited',
985
- 502: 'provider_error',
986
- 503: 'service_unavailable',
987
- 504: 'provider_timeout',
982
+ 400: 'invalid_request',
983
+ 401: 'authentication_failed',
984
+ 403: 'permission_denied',
985
+ 404: 'model_not_found',
986
+ 409: 'idempotency_conflict',
987
+ 413: 'request_too_large',
988
+ 429: 'rate_limited',
989
+ 502: 'provider_error',
990
+ 503: 'service_unavailable',
991
+ 504: 'provider_timeout',
988
992
  };
989
993
 
990
994
  /**
@@ -999,40 +1003,40 @@ const INFERENCE_ERROR_CODE_SET: ReadonlySet<string> = new Set<string>(INFERENCE_
999
1003
 
1000
1004
  /** Read whichever error shape arrived into the one this client throws. */
1001
1005
  function toInferenceError(
1002
- payload: unknown,
1003
- status: number,
1004
- requestIdHeader: string | null,
1006
+ payload: unknown,
1007
+ status: number,
1008
+ requestIdHeader: string | null,
1005
1009
  ): OxyInferenceError {
1006
- const body = (payload ?? {}) as WireInferenceError & { error?: unknown };
1007
-
1008
- // The edge's own shape is the contract error at the top level; the
1009
- // catalogue's is the platform envelope, whose `error` is a string.
1010
- const code =
1011
- typeof body.code === 'string' && INFERENCE_ERROR_CODE_SET.has(body.code)
1012
- ? (body.code as InferenceErrorCode)
1013
- : (STATUS_FALLBACK_CODE[status] ?? 'internal_error');
1014
-
1015
- const message =
1016
- typeof body.message === 'string' && body.message.length > 0
1017
- ? body.message
1018
- : typeof body.error === 'string' && body.error.length > 0
1019
- ? body.error
1020
- : `The inference API answered ${status}.`;
1021
-
1022
- return new OxyInferenceError({
1023
- code,
1024
- message,
1025
- // A body that did not assert retryability is not retryable: the server
1026
- // is the only thing that may say a retry could succeed.
1027
- retryable: body.retryable === true,
1028
- requestId:
1029
- typeof body.requestId === 'string' && body.requestId.length > 0
1030
- ? body.requestId
1031
- : (requestIdHeader ?? ''),
1032
- status,
1033
- ...(body.retryable === true && typeof body.retryAfterMs === 'number'
1034
- ? { retryAfterMs: body.retryAfterMs }
1035
- : {}),
1036
- ...(typeof body.param === 'string' ? { param: body.param } : {}),
1037
- });
1010
+ const body = (payload ?? {}) as WireInferenceError & { error?: unknown };
1011
+
1012
+ // The edge's own shape is the contract error at the top level; the
1013
+ // catalogue's is the platform envelope, whose `error` is a string.
1014
+ const code =
1015
+ typeof body.code === 'string' && INFERENCE_ERROR_CODE_SET.has(body.code)
1016
+ ? (body.code as InferenceErrorCode)
1017
+ : (STATUS_FALLBACK_CODE[status] ?? 'internal_error');
1018
+
1019
+ const message =
1020
+ typeof body.message === 'string' && body.message.length > 0
1021
+ ? body.message
1022
+ : typeof body.error === 'string' && body.error.length > 0
1023
+ ? body.error
1024
+ : `The inference API answered ${status}.`;
1025
+
1026
+ return new OxyInferenceError({
1027
+ code,
1028
+ message,
1029
+ // A body that did not assert retryability is not retryable: the server
1030
+ // is the only thing that may say a retry could succeed.
1031
+ retryable: body.retryable === true,
1032
+ requestId:
1033
+ typeof body.requestId === 'string' && body.requestId.length > 0
1034
+ ? body.requestId
1035
+ : (requestIdHeader ?? ''),
1036
+ status,
1037
+ ...(body.retryable === true && typeof body.retryAfterMs === 'number'
1038
+ ? { retryAfterMs: body.retryAfterMs }
1039
+ : {}),
1040
+ ...(typeof body.param === 'string' ? { param: body.param } : {}),
1041
+ });
1038
1042
  }