@oxy.so/core 4.5.0-oxy-one.1 → 4.5.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.
- package/README.md +1 -1
- package/dist/cjs/HttpService.js +63 -19
- package/dist/cjs/api/accounts.js +10 -3
- package/dist/cjs/api/agency.js +3 -1
- package/dist/cjs/api/apps.js +15 -4
- package/dist/cjs/api/assets.js +39 -11
- package/dist/cjs/api/auth.js +28 -8
- package/dist/cjs/api/billing.js +24 -7
- package/dist/cjs/api/civic.js +14 -4
- package/dist/cjs/api/follows.js +9 -2
- package/dist/cjs/api/identity.js +31 -9
- package/dist/cjs/api/nodes.js +3 -1
- package/dist/cjs/api/privacy.js +8 -2
- package/dist/cjs/api/session.js +15 -4
- package/dist/cjs/api/store.js +15 -5
- package/dist/cjs/api/topics.js +10 -3
- package/dist/cjs/api/users.js +16 -4
- package/dist/cjs/boot/sessionColdBoot.js +40 -13
- package/dist/cjs/constants/version.js +6 -6
- package/dist/cjs/crypto/identityMarker.js +3 -1
- package/dist/cjs/crypto/identityProof.js +6 -1
- package/dist/cjs/crypto/keyManager.js +87 -32
- package/dist/cjs/crypto/recoveryPhrase.js +2 -2
- package/dist/cjs/crypto/signatureService.js +3 -4
- package/dist/cjs/i18n/index.js +16 -3
- package/dist/cjs/i18n/locales/ar-SA.js +366 -366
- package/dist/cjs/i18n/locales/ca-ES.js +366 -366
- package/dist/cjs/i18n/locales/de-DE.js +366 -366
- package/dist/cjs/i18n/locales/en-US.js +1994 -1994
- package/dist/cjs/i18n/locales/es-ES.js +1994 -1994
- package/dist/cjs/i18n/locales/fr-FR.js +366 -366
- package/dist/cjs/i18n/locales/it-IT.js +366 -366
- package/dist/cjs/i18n/locales/ja-JP.js +366 -366
- package/dist/cjs/i18n/locales/ko-KR.js +366 -366
- package/dist/cjs/i18n/locales/pt-PT.js +366 -366
- package/dist/cjs/i18n/locales/zh-CN.js +366 -366
- package/dist/cjs/inference/OxyInferenceClient.js +12 -18
- package/dist/cjs/models/interfaces.js +11 -11
- package/dist/cjs/server/OxyServer.js +86 -28
- package/dist/cjs/server/agentAccount.js +6 -2
- package/dist/cjs/server/aliaMachineCredential.js +39 -14
- package/dist/cjs/server/auth.js +5 -4
- package/dist/cjs/server/capabilityTicket.js +5 -5
- package/dist/cjs/server/index.js +2 -1
- package/dist/cjs/server/liveCapabilityTicket.js +4 -1
- package/dist/cjs/server/middleware.js +61 -42
- package/dist/cjs/server/namespaces.js +28 -7
- package/dist/cjs/server/requesterAssertion.js +36 -30
- package/dist/cjs/server/serviceTokenLifetime.js +7 -4
- package/dist/cjs/server/traffic.js +53 -14
- package/dist/cjs/server/trafficNodeHttp.js +33 -8
- package/dist/cjs/server/workloadIdentity.client.js +4 -0
- package/dist/cjs/server/workloadIdentity.js +28 -1
- package/dist/cjs/session/SessionClient.js +54 -17
- package/dist/cjs/session/accountDelegationAccess.js +4 -6
- package/dist/cjs/session/accountDialogController.js +1 -1
- package/dist/cjs/session/authStateStore.js +18 -9
- package/dist/cjs/session/identityPin.js +6 -2
- package/dist/cjs/session/refresh.js +81 -23
- package/dist/cjs/session/sharedDeviceCredential.js +28 -15
- package/dist/cjs/shared/utils/errorUtils.js +1 -1
- package/dist/cjs/shared/utils/networkUtils.js +1 -1
- package/dist/cjs/shared/utils/themeUtils.js +1 -1
- package/dist/cjs/utils/accountCacheSweep.js +3 -1
- package/dist/cjs/utils/accountUtils.js +1 -3
- package/dist/cjs/utils/asyncUtils.js +1 -1
- package/dist/cjs/utils/authHelpers.js +2 -1
- package/dist/cjs/utils/avatarUtils.js +4 -2
- package/dist/cjs/utils/cache.js +1 -1
- package/dist/cjs/utils/cacheKey.js +3 -1
- package/dist/cjs/utils/deviceManager.js +4 -4
- package/dist/cjs/utils/errorUtils.js +6 -4
- package/dist/cjs/utils/languageUtils.js +404 -57
- package/dist/cjs/utils/profileLinks.js +1 -3
- package/dist/cjs/utils/requestUtils.js +7 -1
- package/dist/cjs/utils/userIdentity.js +1 -1
- package/dist/cjs/utils/validationUtils.js +5 -6
- package/dist/esm/HttpService.js +64 -20
- package/dist/esm/api/accounts.js +11 -4
- package/dist/esm/api/agency.js +3 -1
- package/dist/esm/api/apps.js +15 -4
- package/dist/esm/api/assets.js +39 -11
- package/dist/esm/api/auth.js +28 -8
- package/dist/esm/api/billing.js +25 -8
- package/dist/esm/api/civic.js +14 -4
- package/dist/esm/api/follows.js +9 -2
- package/dist/esm/api/identity.js +31 -9
- package/dist/esm/api/nodes.js +3 -1
- package/dist/esm/api/privacy.js +8 -2
- package/dist/esm/api/session.js +15 -4
- package/dist/esm/api/store.js +15 -5
- package/dist/esm/api/topics.js +10 -3
- package/dist/esm/api/users.js +16 -4
- package/dist/esm/boot/sessionColdBoot.js +41 -14
- package/dist/esm/constants/version.js +6 -6
- package/dist/esm/crypto/identityMarker.js +3 -1
- package/dist/esm/crypto/identityProof.js +6 -1
- package/dist/esm/crypto/internal.js +1 -1
- package/dist/esm/crypto/keyManager.js +87 -32
- package/dist/esm/crypto/recoveryPhrase.js +2 -2
- package/dist/esm/crypto/signatureService.js +3 -4
- package/dist/esm/i18n/index.js +16 -3
- package/dist/esm/i18n/locales/ar-SA.js +366 -366
- package/dist/esm/i18n/locales/ca-ES.js +366 -366
- package/dist/esm/i18n/locales/de-DE.js +366 -366
- package/dist/esm/i18n/locales/en-US.js +1994 -1994
- package/dist/esm/i18n/locales/es-ES.js +1994 -1994
- package/dist/esm/i18n/locales/fr-FR.js +366 -366
- package/dist/esm/i18n/locales/it-IT.js +366 -366
- package/dist/esm/i18n/locales/ja-JP.js +366 -366
- package/dist/esm/i18n/locales/ko-KR.js +366 -366
- package/dist/esm/i18n/locales/pt-PT.js +366 -366
- package/dist/esm/i18n/locales/zh-CN.js +366 -366
- package/dist/esm/index.js +1 -1
- package/dist/esm/inference/OxyInferenceClient.js +13 -19
- package/dist/esm/models/interfaces.js +11 -11
- package/dist/esm/server/OxyServer.js +87 -29
- package/dist/esm/server/agentAccount.js +6 -2
- package/dist/esm/server/aliaMachineCredential.js +39 -14
- package/dist/esm/server/auth.js +6 -5
- package/dist/esm/server/capabilityTicket.js +6 -6
- package/dist/esm/server/index.js +5 -5
- package/dist/esm/server/liveCapabilityTicket.js +6 -3
- package/dist/esm/server/middleware.js +62 -43
- package/dist/esm/server/namespaces.js +31 -10
- package/dist/esm/server/requesterAssertion.js +36 -30
- package/dist/esm/server/serviceTokenLifetime.js +7 -4
- package/dist/esm/server/traffic.js +55 -16
- package/dist/esm/server/trafficNodeHttp.js +34 -9
- package/dist/esm/server/userInvalidation.js +1 -1
- package/dist/esm/server/workloadIdentity.client.js +3 -0
- package/dist/esm/server/workloadIdentity.js +27 -1
- package/dist/esm/session/SessionClient.js +54 -17
- package/dist/esm/session/accountDelegationAccess.js +5 -7
- package/dist/esm/session/accountDialogController.js +1 -1
- package/dist/esm/session/authStateStore.js +18 -9
- package/dist/esm/session/identityPin.js +6 -2
- package/dist/esm/session/refresh.js +82 -24
- package/dist/esm/session/sharedDeviceCredential.js +28 -15
- package/dist/esm/shared/utils/errorUtils.js +1 -1
- package/dist/esm/shared/utils/networkUtils.js +1 -1
- package/dist/esm/shared/utils/themeUtils.js +1 -1
- package/dist/esm/utils/accountCacheSweep.js +3 -1
- package/dist/esm/utils/accountUtils.js +1 -3
- package/dist/esm/utils/asyncUtils.js +1 -1
- package/dist/esm/utils/authHelpers.js +2 -1
- package/dist/esm/utils/avatarUtils.js +4 -2
- package/dist/esm/utils/cache.js +1 -1
- package/dist/esm/utils/cacheKey.js +3 -1
- package/dist/esm/utils/deviceManager.js +4 -4
- package/dist/esm/utils/errorUtils.js +6 -4
- package/dist/esm/utils/languageUtils.js +404 -57
- package/dist/esm/utils/profileLinks.js +1 -3
- package/dist/esm/utils/requestUtils.js +7 -1
- package/dist/esm/utils/userIdentity.js +1 -1
- package/dist/esm/utils/validationUtils.js +5 -6
- package/dist/types/HttpService.d.ts +1 -1
- package/dist/types/api/accounts.d.ts +1 -1
- package/dist/types/api/apps.d.ts +4 -4
- package/dist/types/crypto/internal.d.ts +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/types/server/index.d.ts +6 -6
- package/dist/types/server/workloadIdentity.client.d.ts +5 -1
- package/dist/types/server/workloadIdentity.d.ts +26 -0
- package/dist/types/session/accountDelegationAccess.d.ts +1 -1
- package/dist/types/session/index.d.ts +1 -1
- package/dist/types/utils/userIdentity.d.ts +1 -1
- package/dist/types/utils/validationUtils.d.ts +4 -5
- package/package.json +2 -3
- package/src/HttpService.ts +183 -53
- package/src/OxyServices.errors.ts +7 -3
- package/src/OxyServices.ts +6 -2
- package/src/api/accounts.ts +76 -26
- package/src/api/agency.ts +24 -9
- package/src/api/appData.ts +12 -2
- package/src/api/apps.ts +81 -31
- package/src/api/assets.ts +113 -43
- package/src/api/auth.ts +145 -77
- package/src/api/billing.ts +112 -65
- package/src/api/civic.ts +61 -19
- package/src/api/contacts.ts +4 -1
- package/src/api/devices.ts +54 -21
- package/src/api/follows.ts +88 -31
- package/src/api/identity.ts +101 -33
- package/src/api/linkedAccounts.ts +26 -8
- package/src/api/nodes.ts +18 -6
- package/src/api/notifications.ts +26 -6
- package/src/api/privacy.ts +42 -8
- package/src/api/reputation.ts +18 -7
- package/src/api/session.ts +23 -5
- package/src/api/store.ts +81 -31
- package/src/api/topics.ts +18 -6
- package/src/api/users.ts +98 -35
- package/src/boot/sessionColdBoot.ts +60 -19
- package/src/civic/payloads.ts +1 -4
- package/src/client/context.ts +6 -1
- package/src/constants/version.ts +7 -6
- package/src/crypto/aead.ts +1 -5
- package/src/crypto/ecdh.ts +1 -4
- package/src/crypto/identityDerivations.ts +5 -1
- package/src/crypto/identityMarker.ts +3 -1
- package/src/crypto/identityProof.ts +6 -1
- package/src/crypto/index.ts +11 -11
- package/src/crypto/internal.ts +6 -1
- package/src/crypto/keyManager.ts +348 -98
- package/src/crypto/recoveryPhrase.ts +14 -9
- package/src/crypto/registrationPow.ts +1 -1
- package/src/crypto/signatureService.ts +14 -6
- package/src/i18n/accountCategoryLabels.ts +1 -4
- package/src/i18n/accountRoleLabels.ts +1 -4
- package/src/i18n/index.ts +25 -7
- package/src/i18n/locales/ar-SA.ts +375 -366
- package/src/i18n/locales/ca-ES.ts +382 -366
- package/src/i18n/locales/de-DE.ts +387 -366
- package/src/i18n/locales/en-US.ts +2034 -1994
- package/src/i18n/locales/es-ES.ts +2043 -1994
- package/src/i18n/locales/fr-FR.ts +387 -366
- package/src/i18n/locales/it-IT.ts +380 -366
- package/src/i18n/locales/ja-JP.ts +380 -366
- package/src/i18n/locales/ko-KR.ts +377 -366
- package/src/i18n/locales/pt-PT.ts +383 -366
- package/src/i18n/locales/zh-CN.ts +373 -366
- package/src/i18n/trustTierLabels.ts +1 -4
- package/src/index.ts +371 -349
- package/src/inference/OxyInferenceClient.ts +784 -780
- package/src/models/interfaces.ts +30 -27
- package/src/server/OxyServer.ts +227 -68
- package/src/server/agentAccount.ts +8 -2
- package/src/server/aliaMachineCredential.ts +57 -16
- package/src/server/auth.ts +9 -5
- package/src/server/capabilityTicket.ts +30 -21
- package/src/server/index.ts +29 -6
- package/src/server/liveCapabilityTicket.ts +31 -9
- package/src/server/middleware.ts +212 -121
- package/src/server/namespaces.ts +247 -94
- package/src/server/rateLimit.ts +2 -4
- package/src/server/requesterAssertion.ts +93 -54
- package/src/server/serviceTokenLifetime.ts +9 -4
- package/src/server/traffic.ts +117 -23
- package/src/server/trafficNodeHttp.ts +61 -14
- package/src/server/userInvalidation.ts +1 -4
- package/src/server/workloadIdentity.client.ts +12 -1
- package/src/server/workloadIdentity.ts +52 -5
- package/src/session/SessionClient.ts +170 -49
- package/src/session/accountDelegationAccess.ts +24 -31
- package/src/session/accountDialogController.ts +59 -14
- package/src/session/accountSwitchTargets.ts +10 -15
- package/src/session/authStateStore.ts +121 -81
- package/src/session/deviceDirectory.ts +1 -3
- package/src/session/identityPin.ts +10 -11
- package/src/session/identitySession.ts +3 -1
- package/src/session/index.ts +87 -82
- package/src/session/refresh.ts +138 -44
- package/src/session/sessionClientHost.ts +1 -3
- package/src/session/sharedDeviceCredential.ts +35 -14
- package/src/session/socketLoader.ts +9 -2
- package/src/shared/utils/errorUtils.ts +5 -2
- package/src/shared/utils/networkUtils.ts +5 -8
- package/src/shared/utils/themeUtils.ts +2 -6
- package/src/types/buffer.d.ts +66 -21
- package/src/types/expo-secure-store.d.ts +5 -1
- package/src/utils/accountCacheSweep.ts +4 -5
- package/src/utils/accountUtils.ts +107 -108
- package/src/utils/apiUtils.ts +3 -3
- package/src/utils/asyncUtils.ts +2 -2
- package/src/utils/authHelpers.ts +10 -5
- package/src/utils/avatarUtils.ts +11 -6
- package/src/utils/cache.ts +9 -10
- package/src/utils/cacheKey.ts +5 -1
- package/src/utils/coldBoot.ts +1 -3
- package/src/utils/commonsDelivery.ts +1 -4
- package/src/utils/deviceManager.ts +15 -14
- package/src/utils/errorUtils.ts +68 -86
- package/src/utils/languageUtils.ts +407 -58
- package/src/utils/oauthPkce.ts +1 -4
- package/src/utils/platform.ts +0 -1
- package/src/utils/profileLinks.ts +2 -5
- package/src/utils/requestUtils.ts +18 -14
- package/src/utils/sessionUtils.ts +34 -33
- package/src/utils/userHandle.ts +3 -1
- package/src/utils/userIdentity.ts +1 -1
- 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 {
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
162
|
-
|
|
165
|
+
| { readonly model: string; readonly routingProfileId?: never }
|
|
166
|
+
| { readonly routingProfileId: string; readonly model?: never }
|
|
163
167
|
) & {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
readonly audio: Uint8Array;
|
|
176
|
+
readonly mediaType: InferenceAudioMediaType;
|
|
177
|
+
readonly requestId: string;
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
export interface OxyInferenceRequestOptions {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
)
|
|
697
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
835
|
-
|
|
838
|
+
readonly name: string;
|
|
839
|
+
readonly data: string;
|
|
836
840
|
}
|
|
837
841
|
|
|
838
842
|
function protocolError(message: string, requestId?: string): OxyInferenceProtocolError {
|
|
839
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
863
|
-
|
|
866
|
+
body: ReadableStream<Uint8Array>,
|
|
867
|
+
requestId: string,
|
|
864
868
|
): AsyncGenerator<RawInferenceStreamFrame> {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1006
|
+
payload: unknown,
|
|
1007
|
+
status: number,
|
|
1008
|
+
requestIdHeader: string | null,
|
|
1005
1009
|
): OxyInferenceError {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
}
|