@oxy.so/core 4.0.0 → 4.1.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.
@@ -65,6 +65,7 @@ var _OxyInferenceClient_instances, _OxyInferenceClient_baseURL, _OxyInferenceCli
65
65
  Object.defineProperty(exports, "__esModule", { value: true });
66
66
  exports.OxyInferenceClient = exports.OxyInferenceProtocolError = exports.OxyInferenceError = exports.OXY_INFERENCE_BASE_URL = void 0;
67
67
  const contracts_1 = require("@oxy.so/contracts");
68
+ const contracts_2 = require("@oxy.so/contracts");
68
69
  /** The base URL of the Oxy API, when a caller names none. */
69
70
  exports.OXY_INFERENCE_BASE_URL = 'https://api.oxy.so';
70
71
  /**
@@ -175,7 +176,7 @@ class OxyInferenceClient {
175
176
  * "no such model".
176
177
  */
177
178
  async getModel(modelId, options = {}) {
178
- const parsed = contracts_1.modelIdSchema.safeParse(modelId);
179
+ const parsed = contracts_2.modelIdSchema.safeParse(modelId);
179
180
  if (!parsed.success) {
180
181
  throw new Error(`Not a canonical model id: ${modelId}. Expected <publisher>/<model>, e.g. acme/some-model.`);
181
182
  }
@@ -212,6 +213,23 @@ class OxyInferenceClient {
212
213
  : { delegatedUserId: options.delegatedUserId }),
213
214
  });
214
215
  }
216
+ /** Typed nonstreaming decisions. Idempotency conflicts are never replayed or retried. */
217
+ async decide(request, options = {}) {
218
+ const input = contracts_1.decisionRequestSchema.parse(request);
219
+ const payload = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'POST', '/v1/decisions', {
220
+ body: input,
221
+ bindRequestId: true,
222
+ signal: options.signal,
223
+ idempotencyKey: options.idempotencyKey,
224
+ delegatedUserId: options.delegatedUserId,
225
+ });
226
+ const parsed = contracts_1.decisionSuccessSchema.safeParse(payload);
227
+ if (!parsed.success || parsed.data.model !== input.model ||
228
+ !(0, contracts_1.decisionAnswersMatch)(input, parsed.data.data)) {
229
+ throw new OxyInferenceProtocolError('Invalid or mismatched decisions response.');
230
+ }
231
+ return parsed.data;
232
+ }
215
233
  /** Generate audio bytes through Oxy's authenticated inference edge. */
216
234
  async speech(request, options = {}) {
217
235
  const response = await __classPrivateFieldGet(this, _OxyInferenceClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _OxyInferenceClient_baseURL, "f")}/v1/audio/speech`, {
@@ -232,7 +250,7 @@ class OxyInferenceClient {
232
250
  if (!response.ok) {
233
251
  throw toInferenceError(await response.json().catch(() => undefined), response.status, requestId);
234
252
  }
235
- const mediaType = contracts_1.inferenceAudioMediaTypeSchema.safeParse(mediaTypeOf(response));
253
+ const mediaType = contracts_2.inferenceAudioMediaTypeSchema.safeParse(mediaTypeOf(response));
236
254
  if (!mediaType.success || !response.body) {
237
255
  await cancelUnreadResponse(response);
238
256
  throw protocolError('The inference API returned no supported audio body.', requestId);
@@ -249,7 +267,7 @@ class OxyInferenceClient {
249
267
  break;
250
268
  }
251
269
  size += next.value.byteLength;
252
- if (size > contracts_1.MAX_INFERENCE_AUDIO_BYTES)
270
+ if (size > contracts_2.MAX_INFERENCE_AUDIO_BYTES)
253
271
  throw protocolError('The inference audio exceeds the response limit.', requestId);
254
272
  chunks.push(next.value);
255
273
  }
@@ -332,7 +350,7 @@ class OxyInferenceClient {
332
350
  catch (_error) {
333
351
  throw protocolError(`The inference API sent a ${frame.name || 'unnamed'} SSE event that is not JSON.`, requestId);
334
352
  }
335
- const parsed = contracts_1.inferenceStreamEventSchema.safeParse(payload);
353
+ const parsed = contracts_2.inferenceStreamEventSchema.safeParse(payload);
336
354
  if (!parsed.success) {
337
355
  const path = parsed.error.issues[0]?.path;
338
356
  throw protocolError(`The inference API sent an event outside the published stream contract at ${path === undefined || path.length === 0 ? 'an unknown field' : path.join('.')}.`, requestId);
@@ -428,6 +446,13 @@ async function _OxyInferenceClient_request(method, path, options) {
428
446
  if (!response.ok) {
429
447
  throw toInferenceError(payload, response.status, response.headers.get('X-Oxy-Request-Id'));
430
448
  }
449
+ if (options.bindRequestId) {
450
+ const requestId = response.headers.get('X-Oxy-Request-Id');
451
+ if (!requestId || typeof payload !== 'object' || payload === null ||
452
+ !('requestId' in payload) || payload.requestId !== requestId) {
453
+ throw new OxyInferenceProtocolError('Decisions response request ID does not match its header.', requestId ?? undefined);
454
+ }
455
+ }
431
456
  return payload;
432
457
  };
433
458
  /** Bound one public SSE event before parsing it as JSON. */
@@ -574,7 +599,7 @@ const STATUS_FALLBACK_CODE = {
574
599
  * together — so an unrecognised one falls back to the status map instead of
575
600
  * being asserted into the type.
576
601
  */
577
- const INFERENCE_ERROR_CODE_SET = new Set(contracts_1.INFERENCE_ERROR_CODES);
602
+ const INFERENCE_ERROR_CODE_SET = new Set(contracts_2.INFERENCE_ERROR_CODES);
578
603
  /** Read whichever error shape arrived into the one this client throws. */
579
604
  function toInferenceError(payload, status, requestIdHeader) {
580
605
  const body = (payload ?? {});
@@ -61,6 +61,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
61
61
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
62
62
  };
63
63
  var _OxyInferenceClient_instances, _OxyInferenceClient_baseURL, _OxyInferenceClient_credential, _OxyInferenceClient_fetch, _OxyInferenceClient_bearer, _OxyInferenceClient_headers, _OxyInferenceClient_request;
64
+ import { decisionRequestSchema, decisionSuccessSchema, decisionAnswersMatch } from '@oxy.so/contracts';
64
65
  import { INFERENCE_ERROR_CODES, MAX_INFERENCE_AUDIO_BYTES, inferenceAudioMediaTypeSchema, inferenceStreamEventSchema, modelIdSchema, } from '@oxy.so/contracts';
65
66
  /** The base URL of the Oxy API, when a caller names none. */
66
67
  export const OXY_INFERENCE_BASE_URL = 'https://api.oxy.so';
@@ -207,6 +208,23 @@ export class OxyInferenceClient {
207
208
  : { delegatedUserId: options.delegatedUserId }),
208
209
  });
209
210
  }
211
+ /** Typed nonstreaming decisions. Idempotency conflicts are never replayed or retried. */
212
+ async decide(request, options = {}) {
213
+ const input = decisionRequestSchema.parse(request);
214
+ const payload = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'POST', '/v1/decisions', {
215
+ body: input,
216
+ bindRequestId: true,
217
+ signal: options.signal,
218
+ idempotencyKey: options.idempotencyKey,
219
+ delegatedUserId: options.delegatedUserId,
220
+ });
221
+ const parsed = decisionSuccessSchema.safeParse(payload);
222
+ if (!parsed.success || parsed.data.model !== input.model ||
223
+ !decisionAnswersMatch(input, parsed.data.data)) {
224
+ throw new OxyInferenceProtocolError('Invalid or mismatched decisions response.');
225
+ }
226
+ return parsed.data;
227
+ }
210
228
  /** Generate audio bytes through Oxy's authenticated inference edge. */
211
229
  async speech(request, options = {}) {
212
230
  const response = await __classPrivateFieldGet(this, _OxyInferenceClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _OxyInferenceClient_baseURL, "f")}/v1/audio/speech`, {
@@ -422,6 +440,13 @@ async function _OxyInferenceClient_request(method, path, options) {
422
440
  if (!response.ok) {
423
441
  throw toInferenceError(payload, response.status, response.headers.get('X-Oxy-Request-Id'));
424
442
  }
443
+ if (options.bindRequestId) {
444
+ const requestId = response.headers.get('X-Oxy-Request-Id');
445
+ if (!requestId || typeof payload !== 'object' || payload === null ||
446
+ !('requestId' in payload) || payload.requestId !== requestId) {
447
+ throw new OxyInferenceProtocolError('Decisions response request ID does not match its header.', requestId ?? undefined);
448
+ }
449
+ }
425
450
  return payload;
426
451
  };
427
452
  /** Bound one public SSE event before parsing it as JSON. */
@@ -49,6 +49,7 @@
49
49
  * schema, so a rename on either side fails a build rather than a customer's
50
50
  * request.
51
51
  */
52
+ import { type DecisionRequest, type DecisionSuccess } from '@oxy.so/contracts';
52
53
  import type { CurrencyCode, ExactDecimal, InferenceEnvironment, InferenceErrorCode, InferenceFinishReason, InferenceMessage, InferenceReasoning, InferenceRequestOutcome, InferenceAudioMediaType, InferenceStreamEvent, InferenceSpeechParameters, ModelCatalogueEntry, ResponseFormat, RoutingPolicyReference, RoutingProfile, ToolChoice, ToolDefinition, UnitPrice, UsageQuantity, UsageSource } from '@oxy.so/contracts';
53
54
  /** The base URL of the Oxy API, when a caller names none. */
54
55
  export declare const OXY_INFERENCE_BASE_URL = "https://api.oxy.so";
@@ -336,6 +337,8 @@ export declare class OxyInferenceClient {
336
337
  * `code`, `retryable` and `requestId`.
337
338
  */
338
339
  respond(request: OxyResponsesRequest, options?: OxyInferenceRequestOptions): Promise<OxyInferenceResponse>;
340
+ /** Typed nonstreaming decisions. Idempotency conflicts are never replayed or retried. */
341
+ decide(request: DecisionRequest, options?: OxyInferenceRequestOptions): Promise<DecisionSuccess>;
339
342
  /** Generate audio bytes through Oxy's authenticated inference edge. */
340
343
  speech(request: OxySpeechRequest, options?: OxyInferenceRequestOptions): Promise<OxySpeechResponse>;
341
344
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/core",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -179,8 +179,8 @@
179
179
  "@oxy.so/telemetry": "^1.2.0",
180
180
  "@noble/ciphers": "^1.3.0",
181
181
  "@noble/hashes": "^1.8.0",
182
- "@oxy.so/contracts": "^4.4.0",
183
- "@oxy.so/protocol": "^1.2.0",
182
+ "@oxy.so/contracts": "^4.7.0",
183
+ "@oxy.so/protocol": "^1.2.1",
184
184
  "@scure/bip39": "^1.6.0",
185
185
  "buffer": "^6.0.3",
186
186
  "express-rate-limit": "^8.6.0",
@@ -50,6 +50,8 @@
50
50
  * request.
51
51
  */
52
52
 
53
+ import { decisionRequestSchema, decisionSuccessSchema, decisionAnswersMatch, type DecisionRequest, type DecisionSuccess } from '@oxy.so/contracts';
54
+
53
55
  import type {
54
56
  CurrencyCode,
55
57
  ExactDecimal,
@@ -471,6 +473,27 @@ export class OxyInferenceClient {
471
473
  });
472
474
  }
473
475
 
476
+ /** Typed nonstreaming decisions. Idempotency conflicts are never replayed or retried. */
477
+ async decide(
478
+ request: DecisionRequest,
479
+ options: OxyInferenceRequestOptions = {},
480
+ ): Promise<DecisionSuccess> {
481
+ const input = decisionRequestSchema.parse(request);
482
+ const payload = await this.#request<unknown>('POST', '/v1/decisions', {
483
+ body: input,
484
+ bindRequestId: true,
485
+ signal: options.signal,
486
+ idempotencyKey: options.idempotencyKey,
487
+ delegatedUserId: options.delegatedUserId,
488
+ });
489
+ const parsed = decisionSuccessSchema.safeParse(payload);
490
+ if (!parsed.success || parsed.data.model !== input.model ||
491
+ !decisionAnswersMatch(input, parsed.data.data)) {
492
+ throw new OxyInferenceProtocolError('Invalid or mismatched decisions response.');
493
+ }
494
+ return parsed.data;
495
+ }
496
+
474
497
  /** Generate audio bytes through Oxy's authenticated inference edge. */
475
498
  async speech(
476
499
  request: OxySpeechRequest,
@@ -728,6 +751,7 @@ export class OxyInferenceClient {
728
751
  path: string,
729
752
  options: {
730
753
  body?: unknown;
754
+ bindRequestId?: boolean;
731
755
  signal?: AbortSignal;
732
756
  idempotencyKey?: string;
733
757
  delegatedUserId?: string;
@@ -758,6 +782,13 @@ export class OxyInferenceClient {
758
782
  );
759
783
  }
760
784
 
785
+ if (options.bindRequestId) {
786
+ const requestId = response.headers.get('X-Oxy-Request-Id');
787
+ if (!requestId || typeof payload !== 'object' || payload === null ||
788
+ !('requestId' in payload) || payload.requestId !== requestId) {
789
+ throw new OxyInferenceProtocolError('Decisions response request ID does not match its header.', requestId ?? undefined);
790
+ }
791
+ }
761
792
  return payload as T;
762
793
  }
763
794
  }