@optiqio/qbrix 0.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.
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/dist/index.cjs +342 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +117 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +326 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Optiq (Mustafa Samed Eskin)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<strong>qbrix</strong> — JavaScript/TypeScript SDK for the <a href="https://qbrix.io">Qbrix</a> platform.
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@optiqio/qbrix"><img src="https://img.shields.io/npm/v/@optiqio/qbrix.svg?logo=npm&color=cb3837" alt="npm version"></a>
|
|
7
|
+
<img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/eskinmi/323280689f4d0bb8c6a3e10b31e63c8d/raw/qbrix-js-coverage.json" alt="Coverage">
|
|
8
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
9
|
+
<img src="https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white" alt="Node >=18">
|
|
10
|
+
<img src="https://img.shields.io/badge/types-included-3178C6?logo=typescript&logoColor=white" alt="TypeScript types included">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
A tiny, isomorphic SDK for multi-armed-bandit **selection** and **feedback** — `select` an arm, render it, report a `reward`. Works in the browser, Node 18+, Deno, Bun, and edge runtimes, with **zero runtime dependencies**.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @optiqio/qbrix
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { QbrixClient } from "@optiqio/qbrix";
|
|
27
|
+
|
|
28
|
+
const qbrix = new QbrixClient({ apiKey: process.env.QBRIX_API_KEY });
|
|
29
|
+
|
|
30
|
+
// 1. select an arm for this user/context
|
|
31
|
+
const { arm, requestId } = await qbrix.select("homepage-cta", { id: "user-42" });
|
|
32
|
+
|
|
33
|
+
// 2. render the chosen arm
|
|
34
|
+
console.log(`showing: ${arm.name}`);
|
|
35
|
+
|
|
36
|
+
// 3. report the outcome — pass back the requestId from select
|
|
37
|
+
await qbrix.feedback(requestId, 1.0); // e.g. 1 = converted, 0 = no action
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`select` returns the chosen `arm`, a `requestId`, and `isDefault`. The `requestId` is the handle that ties a later `feedback` call back to the decision — hold onto it.
|
|
41
|
+
|
|
42
|
+
## Use it server-side
|
|
43
|
+
|
|
44
|
+
> [!IMPORTANT]
|
|
45
|
+
> Your API key (`optiq_…`) is a **secret**. The SDK sends it as an `X-API-Key` header, so anywhere the client runs, the key goes too. **Bundling it into browser code exposes it to every visitor.** Run `qbrix` on a server (route handler, edge function, backend) and keep the key in an environment variable. Have the browser call _your_ endpoint, not Qbrix directly.
|
|
46
|
+
|
|
47
|
+
The recommended pattern is a thin server-side handler — the key never reaches the client:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// edge / route handler — runs on the server
|
|
51
|
+
import { QbrixClient } from "@optiqio/qbrix";
|
|
52
|
+
|
|
53
|
+
const qbrix = new QbrixClient({ apiKey: process.env.QBRIX_API_KEY });
|
|
54
|
+
|
|
55
|
+
export default async function handler(req: Request): Promise<Response> {
|
|
56
|
+
const { userId } = await req.json();
|
|
57
|
+
const { arm, requestId } = await qbrix.select("homepage-cta", { id: userId });
|
|
58
|
+
return Response.json({ arm, requestId });
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See [`examples/edge-route.ts`](examples/edge-route.ts) and [`examples/node-quickstart.ts`](examples/node-quickstart.ts) for runnable versions.
|
|
63
|
+
|
|
64
|
+
**Browser usage** is supported for trusted or low-stakes contexts (internal tools, prototypes), but it is a deliberate trade-off: a key shipped to the browser is public. Prefer a server-side proxy for anything user-facing.
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
### `new QbrixClient(options?)`
|
|
69
|
+
|
|
70
|
+
| Option | Type | Default | Env fallback |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| `apiKey` | `string` | — | `QBRIX_API_KEY` |
|
|
73
|
+
| `baseUrl` | `string` | `http://localhost:8080` | `QBRIX_BASE_URL` |
|
|
74
|
+
| `timeout` | `number` (ms) | `30000` | — |
|
|
75
|
+
| `maxRetries` | `number` | `2` | — |
|
|
76
|
+
| `retryOn` | `number[]` | `[429, 502, 503, 504]` | — |
|
|
77
|
+
| `fetch` | `typeof fetch` | runtime global | — |
|
|
78
|
+
| `headers` | `Record<string, string>` | `{}` | — |
|
|
79
|
+
| `logger` | `QbrixLogger` | console (when a level is active) | — |
|
|
80
|
+
| `logLevel` | `LogLevel` | `"off"` | `QBRIX_LOG`, `QBRIX_DEBUG` |
|
|
81
|
+
|
|
82
|
+
Resolution order per option: explicit argument → environment variable → default.
|
|
83
|
+
|
|
84
|
+
### `select(experimentId, context)`
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
select(experimentId: string, context: Context): Promise<SelectResult>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
interface Context {
|
|
92
|
+
id: string; // required — a stable user/session identifier
|
|
93
|
+
vector?: number[]; // optional feature vector
|
|
94
|
+
metadata?: Record<string, unknown>; // optional arbitrary attributes
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface SelectResult {
|
|
98
|
+
arm: { id: string; name: string; index: number };
|
|
99
|
+
requestId: string; // pass this back into feedback()
|
|
100
|
+
isDefault: boolean; // true when the platform returned the fallback arm
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `feedback(requestId, reward)`
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
feedback(requestId: string, reward: number): Promise<void>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Reports the outcome for a prior `select`. `requestId` is the value returned by that `select`; `reward` is the observed signal (e.g. `1` for a conversion, `0` for none — any numeric reward your experiment defines).
|
|
111
|
+
|
|
112
|
+
### Errors
|
|
113
|
+
|
|
114
|
+
Every failure throws a typed error from the `QbrixError` hierarchy — catch the ones you care about with `instanceof`.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import {
|
|
118
|
+
QbrixAPIError,
|
|
119
|
+
RateLimitedError,
|
|
120
|
+
AuthenticationError,
|
|
121
|
+
QbrixTimeoutError,
|
|
122
|
+
} from "@optiqio/qbrix";
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const { arm, requestId } = await qbrix.select("homepage-cta", { id: "user-42" });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err instanceof RateLimitedError) {
|
|
128
|
+
console.warn(`rate limited; retry after ${err.retryAfter}s`);
|
|
129
|
+
} else if (err instanceof AuthenticationError) {
|
|
130
|
+
throw new Error("check your QBRIX_API_KEY");
|
|
131
|
+
} else if (err instanceof QbrixTimeoutError) {
|
|
132
|
+
// request exceeded `timeout`
|
|
133
|
+
} else if (err instanceof QbrixAPIError) {
|
|
134
|
+
console.error(`qbrix ${err.status} ${err.code}: ${err.detail}`);
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- **`QbrixError`** — base class for everything thrown by the SDK.
|
|
141
|
+
- **`QbrixAPIError`** — the proxy returned a non-2xx response. Carries `status`, `detail`, a machine-readable `code`, and optional `context`. Status-specific subclasses: `BadRequestError` (400), `AuthenticationError` (401), `ForbiddenError` (403), `NotFoundError` (404), `ConflictError` (409), `RateLimitedError` (429, adds `retryAfter`), `InternalServerError` (500), `BadGatewayError` (502), `ServiceUnavailableError` (503), `GatewayTimeoutError` (504).
|
|
142
|
+
- **`QbrixConnectionError`** — the request never completed (network failure).
|
|
143
|
+
- **`QbrixTimeoutError`** — the request exceeded `timeout`.
|
|
144
|
+
|
|
145
|
+
### Logging
|
|
146
|
+
|
|
147
|
+
The SDK is **silent by default**. Turn on logging by setting `logLevel` (`"debug" | "info" | "warn" | "error" | "off"`), injecting a `logger`, or via the `QBRIX_LOG` / `QBRIX_DEBUG` environment variables — handy for debugging a running deployment without a code change.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// route to the console at a chosen verbosity
|
|
151
|
+
new QbrixClient({ apiKey, logLevel: "debug" });
|
|
152
|
+
|
|
153
|
+
// or plug in your own sink (pino, winston, …) — receives only request metadata
|
|
154
|
+
new QbrixClient({
|
|
155
|
+
apiKey,
|
|
156
|
+
logger: {
|
|
157
|
+
debug: (msg, ctx) => log.debug(ctx, msg),
|
|
158
|
+
info: (msg, ctx) => log.info(ctx, msg),
|
|
159
|
+
warn: (msg, ctx) => log.warn(ctx, msg),
|
|
160
|
+
error: (msg, ctx) => log.error(ctx, msg),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
QBRIX_LOG=warn node app.js # or QBRIX_DEBUG=1 for full debug output
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The transport logs each request attempt and success at `debug`, retries at `warn`, and failures (give-up, timeout, connection) at `error`. Log context is limited to method, path, status, and attempt counts — it **never** includes your API key, headers, or request/response bodies.
|
|
170
|
+
|
|
171
|
+
## Runtime support
|
|
172
|
+
|
|
173
|
+
Runs unmodified on **Node 18+, Deno, Bun, edge runtimes, and the browser** — built on universal primitives (`fetch`, `AbortController`) with zero runtime dependencies. Ships dual ESM + CJS with bundled type declarations.
|
|
174
|
+
|
|
175
|
+
Full documentation: [qbrix.io/docs](https://qbrix.io/docs).
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm install
|
|
181
|
+
npm run build # dual ESM + CJS + .d.ts via tsup
|
|
182
|
+
npm run test # vitest
|
|
183
|
+
npm run typecheck # tsc --noEmit
|
|
184
|
+
npm run lint # biome
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/logger.ts
|
|
4
|
+
var RANK = {
|
|
5
|
+
debug: 10,
|
|
6
|
+
info: 20,
|
|
7
|
+
warn: 30,
|
|
8
|
+
error: 40,
|
|
9
|
+
off: 100
|
|
10
|
+
};
|
|
11
|
+
function shouldLog(configured, event) {
|
|
12
|
+
return RANK[event] >= RANK[configured];
|
|
13
|
+
}
|
|
14
|
+
var consoleLogger = {
|
|
15
|
+
debug: (message, context) => context ? console.debug(message, context) : console.debug(message),
|
|
16
|
+
info: (message, context) => context ? console.info(message, context) : console.info(message),
|
|
17
|
+
warn: (message, context) => context ? console.warn(message, context) : console.warn(message),
|
|
18
|
+
error: (message, context) => context ? console.error(message, context) : console.error(message)
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
22
|
+
var LOG_LEVELS = ["debug", "info", "warn", "error", "off"];
|
|
23
|
+
function resolveLogLevel(option, hasLogger) {
|
|
24
|
+
if (option) return option;
|
|
25
|
+
const env = readEnv("QBRIX_LOG");
|
|
26
|
+
if (env && LOG_LEVELS.includes(env)) return env;
|
|
27
|
+
if (readEnv("QBRIX_DEBUG")) return "debug";
|
|
28
|
+
return hasLogger ? "debug" : "off";
|
|
29
|
+
}
|
|
30
|
+
var DEFAULTS = {
|
|
31
|
+
baseUrl: "http://localhost:8080",
|
|
32
|
+
timeout: 3e4,
|
|
33
|
+
maxRetries: 2,
|
|
34
|
+
retryOn: [429, 502, 503, 504]
|
|
35
|
+
};
|
|
36
|
+
function readEnv(name) {
|
|
37
|
+
try {
|
|
38
|
+
const env = globalThis.process?.env;
|
|
39
|
+
return env?.[name];
|
|
40
|
+
} catch {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function resolveConfig(options = {}) {
|
|
45
|
+
const logLevel = resolveLogLevel(options.logLevel, options.logger !== void 0);
|
|
46
|
+
const config = {
|
|
47
|
+
apiKey: options.apiKey ?? readEnv("QBRIX_API_KEY"),
|
|
48
|
+
baseUrl: options.baseUrl ?? readEnv("QBRIX_BASE_URL") ?? DEFAULTS.baseUrl,
|
|
49
|
+
timeout: options.timeout ?? DEFAULTS.timeout,
|
|
50
|
+
maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
|
|
51
|
+
retryOn: options.retryOn ?? [...DEFAULTS.retryOn],
|
|
52
|
+
fetch: options.fetch,
|
|
53
|
+
headers: options.headers ?? {},
|
|
54
|
+
logger: options.logger ?? (logLevel === "off" ? void 0 : consoleLogger),
|
|
55
|
+
logLevel
|
|
56
|
+
};
|
|
57
|
+
if (config.timeout <= 0) {
|
|
58
|
+
throw new Error(`qbrix: timeout must be > 0, got ${config.timeout}`);
|
|
59
|
+
}
|
|
60
|
+
if (config.maxRetries < 0) {
|
|
61
|
+
throw new Error(`qbrix: maxRetries must be >= 0, got ${config.maxRetries}`);
|
|
62
|
+
}
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/errors.ts
|
|
67
|
+
var QbrixError = class extends Error {
|
|
68
|
+
constructor(message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = new.target.name;
|
|
71
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var QbrixAPIError = class extends QbrixError {
|
|
75
|
+
status;
|
|
76
|
+
detail;
|
|
77
|
+
code;
|
|
78
|
+
context;
|
|
79
|
+
constructor(status, detail, options) {
|
|
80
|
+
super(`[${status}] ${detail}`);
|
|
81
|
+
this.status = status;
|
|
82
|
+
this.detail = detail;
|
|
83
|
+
this.code = options?.code;
|
|
84
|
+
this.context = options?.context;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var BadRequestError = class extends QbrixAPIError {
|
|
88
|
+
};
|
|
89
|
+
var AuthenticationError = class extends QbrixAPIError {
|
|
90
|
+
};
|
|
91
|
+
var ForbiddenError = class extends QbrixAPIError {
|
|
92
|
+
};
|
|
93
|
+
var NotFoundError = class extends QbrixAPIError {
|
|
94
|
+
};
|
|
95
|
+
var ConflictError = class extends QbrixAPIError {
|
|
96
|
+
};
|
|
97
|
+
var RateLimitedError = class extends QbrixAPIError {
|
|
98
|
+
retryAfter;
|
|
99
|
+
constructor(status, detail, options) {
|
|
100
|
+
super(status, detail, options);
|
|
101
|
+
this.retryAfter = options?.retryAfter;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var InternalServerError = class extends QbrixAPIError {
|
|
105
|
+
};
|
|
106
|
+
var BadGatewayError = class extends QbrixAPIError {
|
|
107
|
+
};
|
|
108
|
+
var ServiceUnavailableError = class extends QbrixAPIError {
|
|
109
|
+
};
|
|
110
|
+
var GatewayTimeoutError = class extends QbrixAPIError {
|
|
111
|
+
};
|
|
112
|
+
var QbrixConnectionError = class extends QbrixError {
|
|
113
|
+
};
|
|
114
|
+
var QbrixTimeoutError = class extends QbrixError {
|
|
115
|
+
};
|
|
116
|
+
var STATUS_TO_ERROR = {
|
|
117
|
+
400: BadRequestError,
|
|
118
|
+
401: AuthenticationError,
|
|
119
|
+
403: ForbiddenError,
|
|
120
|
+
404: NotFoundError,
|
|
121
|
+
409: ConflictError,
|
|
122
|
+
429: RateLimitedError,
|
|
123
|
+
500: InternalServerError,
|
|
124
|
+
502: BadGatewayError,
|
|
125
|
+
503: ServiceUnavailableError,
|
|
126
|
+
504: GatewayTimeoutError
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/mapper.ts
|
|
130
|
+
function toSelectRequest(params) {
|
|
131
|
+
const { id, vector, metadata } = params.context;
|
|
132
|
+
return {
|
|
133
|
+
experiment_id: params.experimentId,
|
|
134
|
+
context: {
|
|
135
|
+
id,
|
|
136
|
+
...vector !== void 0 && { vector },
|
|
137
|
+
...metadata !== void 0 && { metadata }
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function fromSelectResponse(wire) {
|
|
142
|
+
return {
|
|
143
|
+
arm: { id: wire.arm.id, name: wire.arm.name, index: wire.arm.index },
|
|
144
|
+
requestId: wire.request_id,
|
|
145
|
+
isDefault: wire.is_default
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function toFeedbackRequest(params) {
|
|
149
|
+
return {
|
|
150
|
+
request_id: params.requestId,
|
|
151
|
+
reward: params.reward
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/transport.ts
|
|
156
|
+
var RETRY_BASE_DELAY_MS = 500;
|
|
157
|
+
var RETRY_MAX_DELAY_MS = 8e3;
|
|
158
|
+
function emit(config, level, message, context) {
|
|
159
|
+
if (config.logger && shouldLog(config.logLevel, level)) {
|
|
160
|
+
config.logger[level](message, context);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function request(config, method, path, options = {}) {
|
|
164
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
165
|
+
const url = joinUrl(config.baseUrl, path);
|
|
166
|
+
const headers = buildHeaders(config);
|
|
167
|
+
const body = options.body === void 0 ? void 0 : JSON.stringify(options.body);
|
|
168
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
169
|
+
emit(config, "debug", "request attempt", {
|
|
170
|
+
method,
|
|
171
|
+
path,
|
|
172
|
+
attempt: attempt + 1,
|
|
173
|
+
attempts: config.maxRetries + 1
|
|
174
|
+
});
|
|
175
|
+
const timeoutSignal = AbortSignal.timeout(config.timeout);
|
|
176
|
+
const signal = combineSignals(options.signal, timeoutSignal);
|
|
177
|
+
let response;
|
|
178
|
+
try {
|
|
179
|
+
response = await fetchImpl(url, { method, headers, body, signal });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (options.signal?.aborted) throw options.signal.reason ?? err;
|
|
182
|
+
if (timeoutSignal.aborted) {
|
|
183
|
+
emit(config, "error", "request timed out", { method, path });
|
|
184
|
+
throw new QbrixTimeoutError(`qbrix: request timed out after ${config.timeout}ms`);
|
|
185
|
+
}
|
|
186
|
+
emit(config, "error", "request connection error", { method, path });
|
|
187
|
+
throw new QbrixConnectionError(err instanceof Error ? err.message : String(err));
|
|
188
|
+
}
|
|
189
|
+
if (response.ok) {
|
|
190
|
+
emit(config, "debug", "request succeeded", { method, path, status: response.status });
|
|
191
|
+
if (response.status === 204) return void 0;
|
|
192
|
+
const text = await response.text();
|
|
193
|
+
return text ? JSON.parse(text) : void 0;
|
|
194
|
+
}
|
|
195
|
+
const error = await makeApiError(response);
|
|
196
|
+
if (!config.retryOn.includes(response.status) || attempt === config.maxRetries) {
|
|
197
|
+
emit(config, "error", "request failed", { method, path, status: response.status });
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
const delay = retryDelay(attempt, error);
|
|
201
|
+
emit(config, "warn", "request retrying", {
|
|
202
|
+
method,
|
|
203
|
+
path,
|
|
204
|
+
status: response.status,
|
|
205
|
+
attempt: attempt + 1,
|
|
206
|
+
delayMs: Math.round(delay)
|
|
207
|
+
});
|
|
208
|
+
await sleep(delay, options.signal);
|
|
209
|
+
}
|
|
210
|
+
throw new QbrixError("qbrix: request failed");
|
|
211
|
+
}
|
|
212
|
+
function joinUrl(baseUrl, path) {
|
|
213
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
214
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
215
|
+
return `${base}${suffix}`;
|
|
216
|
+
}
|
|
217
|
+
function combineSignals(caller, timeout) {
|
|
218
|
+
if (!caller) return timeout;
|
|
219
|
+
const anyFn = AbortSignal.any;
|
|
220
|
+
if (typeof anyFn === "function") return anyFn([caller, timeout]);
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const forward = (source) => () => controller.abort(source.reason);
|
|
223
|
+
if (caller.aborted) controller.abort(caller.reason);
|
|
224
|
+
else if (timeout.aborted) controller.abort(timeout.reason);
|
|
225
|
+
else {
|
|
226
|
+
caller.addEventListener("abort", forward(caller), { once: true });
|
|
227
|
+
timeout.addEventListener("abort", forward(timeout), { once: true });
|
|
228
|
+
}
|
|
229
|
+
return controller.signal;
|
|
230
|
+
}
|
|
231
|
+
async function makeApiError(response) {
|
|
232
|
+
const raw = await response.text();
|
|
233
|
+
let detail = raw || response.statusText;
|
|
234
|
+
let code;
|
|
235
|
+
let context;
|
|
236
|
+
if (raw) {
|
|
237
|
+
try {
|
|
238
|
+
const parsed = JSON.parse(raw);
|
|
239
|
+
if (typeof parsed.detail === "string") detail = parsed.detail;
|
|
240
|
+
if (typeof parsed.code === "string") code = parsed.code;
|
|
241
|
+
if (parsed.context && typeof parsed.context === "object") {
|
|
242
|
+
context = parsed.context;
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const status = response.status;
|
|
248
|
+
if (status === 429) {
|
|
249
|
+
const header = response.headers.get("Retry-After");
|
|
250
|
+
const seconds = header ? Number.parseFloat(header) : Number.NaN;
|
|
251
|
+
return new RateLimitedError(status, detail, {
|
|
252
|
+
code,
|
|
253
|
+
context,
|
|
254
|
+
retryAfter: Number.isFinite(seconds) ? seconds : void 0
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const ErrorClass = STATUS_TO_ERROR[status] ?? QbrixAPIError;
|
|
258
|
+
return new ErrorClass(status, detail, { code, context });
|
|
259
|
+
}
|
|
260
|
+
function retryDelay(attempt, error) {
|
|
261
|
+
if (error instanceof RateLimitedError && error.retryAfter !== void 0) {
|
|
262
|
+
return Math.min(error.retryAfter * 1e3, RETRY_MAX_DELAY_MS);
|
|
263
|
+
}
|
|
264
|
+
const base = Math.min(RETRY_BASE_DELAY_MS * 2 ** attempt, RETRY_MAX_DELAY_MS);
|
|
265
|
+
return base + Math.random() * base * 0.1;
|
|
266
|
+
}
|
|
267
|
+
function sleep(ms, signal) {
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
if (signal?.aborted) {
|
|
270
|
+
reject(signal.reason);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const timer = setTimeout(() => {
|
|
274
|
+
signal?.removeEventListener("abort", onAbort);
|
|
275
|
+
resolve();
|
|
276
|
+
}, ms);
|
|
277
|
+
function onAbort() {
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
reject(signal?.reason);
|
|
280
|
+
}
|
|
281
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/version.ts
|
|
286
|
+
var VERSION = "0.0.0";
|
|
287
|
+
|
|
288
|
+
// src/client.ts
|
|
289
|
+
function buildHeaders(config) {
|
|
290
|
+
const headers = {
|
|
291
|
+
Accept: "application/json",
|
|
292
|
+
"Content-Type": "application/json"
|
|
293
|
+
};
|
|
294
|
+
if (config.apiKey) {
|
|
295
|
+
headers["X-API-Key"] = config.apiKey;
|
|
296
|
+
}
|
|
297
|
+
if (typeof document === "undefined") {
|
|
298
|
+
headers["User-Agent"] = `qbrix-js/${VERSION}`;
|
|
299
|
+
}
|
|
300
|
+
return { ...headers, ...config.headers };
|
|
301
|
+
}
|
|
302
|
+
var QbrixClient = class {
|
|
303
|
+
config;
|
|
304
|
+
constructor(options = {}) {
|
|
305
|
+
this.config = resolveConfig(options);
|
|
306
|
+
}
|
|
307
|
+
async select(experimentId, context) {
|
|
308
|
+
const body = toSelectRequest({ experimentId, context });
|
|
309
|
+
const wire = await request(
|
|
310
|
+
this.config,
|
|
311
|
+
"POST",
|
|
312
|
+
"/api/v1/agent/select",
|
|
313
|
+
{ body }
|
|
314
|
+
);
|
|
315
|
+
if (wire === void 0) {
|
|
316
|
+
throw new QbrixError("qbrix: select returned no response body");
|
|
317
|
+
}
|
|
318
|
+
return fromSelectResponse(wire);
|
|
319
|
+
}
|
|
320
|
+
async feedback(requestId, reward) {
|
|
321
|
+
const body = toFeedbackRequest({ requestId, reward });
|
|
322
|
+
await request(this.config, "POST", "/api/v1/agent/feedback", { body });
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
exports.AuthenticationError = AuthenticationError;
|
|
327
|
+
exports.BadGatewayError = BadGatewayError;
|
|
328
|
+
exports.BadRequestError = BadRequestError;
|
|
329
|
+
exports.ConflictError = ConflictError;
|
|
330
|
+
exports.ForbiddenError = ForbiddenError;
|
|
331
|
+
exports.GatewayTimeoutError = GatewayTimeoutError;
|
|
332
|
+
exports.InternalServerError = InternalServerError;
|
|
333
|
+
exports.NotFoundError = NotFoundError;
|
|
334
|
+
exports.QbrixAPIError = QbrixAPIError;
|
|
335
|
+
exports.QbrixClient = QbrixClient;
|
|
336
|
+
exports.QbrixConnectionError = QbrixConnectionError;
|
|
337
|
+
exports.QbrixError = QbrixError;
|
|
338
|
+
exports.QbrixTimeoutError = QbrixTimeoutError;
|
|
339
|
+
exports.RateLimitedError = RateLimitedError;
|
|
340
|
+
exports.ServiceUnavailableError = ServiceUnavailableError;
|
|
341
|
+
//# sourceMappingURL=index.cjs.map
|
|
342
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/logger.ts","../src/config.ts","../src/errors.ts","../src/mapper.ts","../src/transport.ts","../src/version.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// pluggable, leveled logging. the sdk is silent by default (logLevel \"off\") and\n// only emits when a level is configured (via the logLevel option, the QBRIX_LOG\n// / QBRIX_DEBUG env vars, or by injecting a logger). the sdk passes only\n// non-sensitive context (method, path, status, attempt counts) — never the api\n// key, headers, or request/response bodies.\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"off\";\n\nexport interface QbrixLogger {\n debug(message: string, context?: Record<string, unknown>): void;\n info(message: string, context?: Record<string, unknown>): void;\n warn(message: string, context?: Record<string, unknown>): void;\n error(message: string, context?: Record<string, unknown>): void;\n}\n\nconst RANK: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n off: 100,\n};\n\n// emit an event at `event` level only when it meets the configured threshold.\nexport function shouldLog(configured: LogLevel, event: Exclude<LogLevel, \"off\">): boolean {\n return RANK[event] >= RANK[configured];\n}\n\n// built-in sink for the env / logLevel opt-in; routes each level to console.\nexport const consoleLogger: QbrixLogger = {\n debug: (message, context) => (context ? console.debug(message, context) : console.debug(message)),\n info: (message, context) => (context ? console.info(message, context) : console.info(message)),\n warn: (message, context) => (context ? console.warn(message, context) : console.warn(message)),\n error: (message, context) => (context ? console.error(message, context) : console.error(message)),\n};\n","import type { QbrixClientOptions } from \"./client\";\nimport { type LogLevel, type QbrixLogger, consoleLogger } from \"./logger\";\n\nexport interface ResolvedConfig {\n apiKey: string | undefined;\n baseUrl: string;\n timeout: number;\n maxRetries: number;\n retryOn: number[];\n fetch: typeof fetch | undefined;\n headers: Record<string, string>;\n logger: QbrixLogger | undefined;\n logLevel: LogLevel;\n}\n\nconst LOG_LEVELS: readonly LogLevel[] = [\"debug\", \"info\", \"warn\", \"error\", \"off\"];\n\n// precedence: explicit option → QBRIX_LOG → QBRIX_DEBUG (sugar for \"debug\") →\n// \"debug\" when a logger is injected (opt-in by passing one) → \"off\" (silent).\nfunction resolveLogLevel(option: LogLevel | undefined, hasLogger: boolean): LogLevel {\n if (option) return option;\n const env = readEnv(\"QBRIX_LOG\");\n if (env && (LOG_LEVELS as readonly string[]).includes(env)) return env as LogLevel;\n if (readEnv(\"QBRIX_DEBUG\")) return \"debug\";\n return hasLogger ? \"debug\" : \"off\";\n}\n\n// todo: check the defaults / might want to add the hosted url here / update the timeout to be low latency etc.\nconst DEFAULTS = {\n baseUrl: \"http://localhost:8080\",\n timeout: 30_000,\n maxRetries: 2,\n retryOn: [429, 502, 503, 504] as number[],\n};\n\n// guarded globalThis access keeps the browser bundle clean and avoids needing `@types/node`.\n// the try/catch matters for deno, where reading `process.env` throws without `--allow-env` —\n// the sdk treats env as unset rather than crashing.\nfunction readEnv(name: string): string | undefined {\n try {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n ?.env;\n return env?.[name];\n } catch {\n return undefined;\n }\n}\n\n// precedence per option: explicit arg → env var → default\nexport function resolveConfig(options: QbrixClientOptions = {}): ResolvedConfig {\n const logLevel = resolveLogLevel(options.logLevel, options.logger !== undefined);\n const config: ResolvedConfig = {\n apiKey: options.apiKey ?? readEnv(\"QBRIX_API_KEY\"),\n baseUrl: options.baseUrl ?? readEnv(\"QBRIX_BASE_URL\") ?? DEFAULTS.baseUrl,\n timeout: options.timeout ?? DEFAULTS.timeout,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryOn: options.retryOn ?? [...DEFAULTS.retryOn],\n fetch: options.fetch,\n headers: options.headers ?? {},\n logger: options.logger ?? (logLevel === \"off\" ? undefined : consoleLogger),\n logLevel,\n };\n\n if (config.timeout <= 0) {\n throw new Error(`qbrix: timeout must be > 0, got ${config.timeout}`);\n }\n if (config.maxRetries < 0) {\n throw new Error(`qbrix: maxRetries must be >= 0, got ${config.maxRetries}`);\n }\n\n return config;\n}\n","import type { ErrorCode } from \"./types\";\n\nexport class QbrixError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n // keep instanceof working when consumers downlevel below es2015\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// `string & {}` keeps autocomplete of the known codes while tolerating codes a\n// newer backend may send before the sdk's generated union catches up.\ninterface ApiErrorOptions {\n code?: ErrorCode | (string & {});\n context?: Record<string, unknown>;\n}\n\nexport class QbrixAPIError extends QbrixError {\n readonly status: number;\n readonly detail: string;\n readonly code: ErrorCode | (string & {}) | undefined;\n readonly context: Record<string, unknown> | undefined;\n\n constructor(status: number, detail: string, options?: ApiErrorOptions) {\n super(`[${status}] ${detail}`);\n this.status = status;\n this.detail = detail;\n this.code = options?.code;\n this.context = options?.context;\n }\n}\n\nexport class BadRequestError extends QbrixAPIError {}\nexport class AuthenticationError extends QbrixAPIError {}\nexport class ForbiddenError extends QbrixAPIError {}\nexport class NotFoundError extends QbrixAPIError {}\nexport class ConflictError extends QbrixAPIError {}\n\ninterface RateLimitedErrorOptions extends ApiErrorOptions {\n retryAfter?: number;\n}\n\nexport class RateLimitedError extends QbrixAPIError {\n readonly retryAfter: number | undefined;\n\n constructor(status: number, detail: string, options?: RateLimitedErrorOptions) {\n super(status, detail, options);\n this.retryAfter = options?.retryAfter;\n }\n}\n\nexport class InternalServerError extends QbrixAPIError {}\nexport class BadGatewayError extends QbrixAPIError {}\nexport class ServiceUnavailableError extends QbrixAPIError {}\nexport class GatewayTimeoutError extends QbrixAPIError {}\n\nexport class QbrixConnectionError extends QbrixError {}\nexport class QbrixTimeoutError extends QbrixError {}\n\ntype ApiErrorConstructor = new (\n status: number,\n detail: string,\n options?: ApiErrorOptions,\n) => QbrixAPIError;\n\nexport const STATUS_TO_ERROR: Record<number, ApiErrorConstructor> = {\n 400: BadRequestError,\n 401: AuthenticationError,\n 403: ForbiddenError,\n 404: NotFoundError,\n 409: ConflictError,\n 429: RateLimitedError,\n 500: InternalServerError,\n 502: BadGatewayError,\n 503: ServiceUnavailableError,\n 504: GatewayTimeoutError,\n};\n","import type { components } from \"./generated\";\nimport type { FeedbackParams, SelectParams, SelectResult } from \"./types\";\n\ntype WireSelectRequest = components[\"schemas\"][\"AgentSelectRequest\"];\ntype WireSelectResponse = components[\"schemas\"][\"AgentSelectResponse\"];\ntype WireFeedbackRequest = components[\"schemas\"][\"AgentFeedbackRequest\"];\n\nexport function toSelectRequest(params: SelectParams): WireSelectRequest {\n const { id, vector, metadata } = params.context;\n return {\n experiment_id: params.experimentId,\n context: {\n id,\n ...(vector !== undefined && { vector }),\n ...(metadata !== undefined && { metadata }),\n },\n };\n}\n\nexport function fromSelectResponse(wire: WireSelectResponse): SelectResult {\n return {\n arm: { id: wire.arm.id, name: wire.arm.name, index: wire.arm.index },\n requestId: wire.request_id,\n isDefault: wire.is_default,\n };\n}\n\nexport function toFeedbackRequest(params: FeedbackParams): WireFeedbackRequest {\n return {\n request_id: params.requestId,\n reward: params.reward,\n };\n}\n","import { buildHeaders } from \"./client\";\nimport type { ResolvedConfig } from \"./config\";\nimport {\n QbrixAPIError,\n QbrixConnectionError,\n QbrixError,\n QbrixTimeoutError,\n RateLimitedError,\n STATUS_TO_ERROR,\n} from \"./errors\";\nimport type { components } from \"./generated\";\nimport { type LogLevel, shouldLog } from \"./logger\";\n\nexport interface RequestOptions {\n body?: unknown;\n signal?: AbortSignal;\n}\n\nconst RETRY_BASE_DELAY_MS = 500;\nconst RETRY_MAX_DELAY_MS = 8_000;\n\nfunction emit(\n config: ResolvedConfig,\n level: Exclude<LogLevel, \"off\">,\n message: string,\n context: Record<string, unknown>,\n): void {\n if (config.logger && shouldLog(config.logLevel, level)) {\n config.logger[level](message, context);\n }\n}\n\nexport async function request<T>(\n config: ResolvedConfig,\n method: string,\n path: string,\n options: RequestOptions = {},\n): Promise<T | undefined> {\n const fetchImpl = config.fetch ?? globalThis.fetch;\n const url = joinUrl(config.baseUrl, path);\n const headers = buildHeaders(config);\n const body = options.body === undefined ? undefined : JSON.stringify(options.body);\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt++) {\n emit(config, \"debug\", \"request attempt\", {\n method,\n path,\n attempt: attempt + 1,\n attempts: config.maxRetries + 1,\n });\n const timeoutSignal = AbortSignal.timeout(config.timeout);\n const signal = combineSignals(options.signal, timeoutSignal);\n\n let response: Response;\n try {\n response = await fetchImpl(url, { method, headers, body, signal });\n } catch (err) {\n // caller-initiated abort takes precedence and propagates unchanged\n if (options.signal?.aborted) throw options.signal.reason ?? err;\n if (timeoutSignal.aborted) {\n emit(config, \"error\", \"request timed out\", { method, path });\n throw new QbrixTimeoutError(`qbrix: request timed out after ${config.timeout}ms`);\n }\n emit(config, \"error\", \"request connection error\", { method, path });\n throw new QbrixConnectionError(err instanceof Error ? err.message : String(err));\n }\n\n if (response.ok) {\n emit(config, \"debug\", \"request succeeded\", { method, path, status: response.status });\n if (response.status === 204) return undefined;\n const text = await response.text();\n return text ? (JSON.parse(text) as T) : undefined;\n }\n\n const error = await makeApiError(response);\n if (!config.retryOn.includes(response.status) || attempt === config.maxRetries) {\n emit(config, \"error\", \"request failed\", { method, path, status: response.status });\n throw error;\n }\n const delay = retryDelay(attempt, error);\n emit(config, \"warn\", \"request retrying\", {\n method,\n path,\n status: response.status,\n attempt: attempt + 1,\n delayMs: Math.round(delay),\n });\n await sleep(delay, options.signal);\n }\n\n // unreachable: the final attempt always returns or throws\n throw new QbrixError(\"qbrix: request failed\");\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n const base = baseUrl.endsWith(\"/\") ? baseUrl.slice(0, -1) : baseUrl;\n const suffix = path.startsWith(\"/\") ? path : `/${path}`;\n return `${base}${suffix}`;\n}\n\nfunction combineSignals(caller: AbortSignal | undefined, timeout: AbortSignal): AbortSignal {\n if (!caller) return timeout;\n const anyFn = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;\n if (typeof anyFn === \"function\") return anyFn([caller, timeout]);\n\n // manual fallback for runtimes without AbortSignal.any\n const controller = new AbortController();\n const forward = (source: AbortSignal) => () => controller.abort(source.reason);\n if (caller.aborted) controller.abort(caller.reason);\n else if (timeout.aborted) controller.abort(timeout.reason);\n else {\n caller.addEventListener(\"abort\", forward(caller), { once: true });\n timeout.addEventListener(\"abort\", forward(timeout), { once: true });\n }\n return controller.signal;\n}\n\ntype WireError = components[\"schemas\"][\"ErrorResponse\"];\n\nasync function makeApiError(response: Response): Promise<QbrixAPIError> {\n // read the body once; fetch streams can't be re-read after JSON.parse fails\n const raw = await response.text();\n let detail = raw || response.statusText;\n let code: WireError[\"code\"] | undefined;\n let context: Record<string, unknown> | undefined;\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Partial<WireError>;\n if (typeof parsed.detail === \"string\") detail = parsed.detail;\n if (typeof parsed.code === \"string\") code = parsed.code;\n if (parsed.context && typeof parsed.context === \"object\") {\n context = parsed.context as Record<string, unknown>;\n }\n } catch {\n // non-json body — keep the raw text as detail\n }\n }\n\n const status = response.status;\n if (status === 429) {\n const header = response.headers.get(\"Retry-After\");\n const seconds = header ? Number.parseFloat(header) : Number.NaN;\n return new RateLimitedError(status, detail, {\n code,\n context,\n retryAfter: Number.isFinite(seconds) ? seconds : undefined,\n });\n }\n\n const ErrorClass = STATUS_TO_ERROR[status] ?? QbrixAPIError;\n return new ErrorClass(status, detail, { code, context });\n}\n\nfunction retryDelay(attempt: number, error: QbrixAPIError): number {\n if (error instanceof RateLimitedError && error.retryAfter !== undefined) {\n return Math.min(error.retryAfter * 1000, RETRY_MAX_DELAY_MS);\n }\n const base = Math.min(RETRY_BASE_DELAY_MS * 2 ** attempt, RETRY_MAX_DELAY_MS);\n return base + Math.random() * base * 0.1;\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n function onAbort() {\n clearTimeout(timer);\n reject(signal?.reason);\n }\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n","// kept in sync with package.json by release tooling (OPT-41).\nexport const VERSION = \"0.0.0\";\n","import type { ResolvedConfig } from \"./config\";\nimport { resolveConfig } from \"./config\";\nimport { QbrixError } from \"./errors\";\nimport type { components } from \"./generated\";\nimport type { LogLevel, QbrixLogger } from \"./logger\";\nimport { fromSelectResponse, toFeedbackRequest, toSelectRequest } from \"./mapper\";\nimport { request } from \"./transport\";\nimport type { Context, SelectResult } from \"./types\";\nimport { VERSION } from \"./version\";\n\nexport interface QbrixClientOptions {\n /** qbrix api key (prefix `optiq_`). falls back to `QBRIX_API_KEY`. */\n apiKey?: string;\n /** base url of the qbrix proxy. falls back to `QBRIX_BASE_URL`. */\n baseUrl?: string;\n /** request timeout in milliseconds. */\n timeout?: number;\n /** max retry attempts on retryable status codes. */\n maxRetries?: number;\n /** status codes to retry. */\n retryOn?: number[];\n /** custom fetch implementation, injectable for tests and custom runtimes. */\n fetch?: typeof fetch;\n /** extra headers merged into every request; user headers override the defaults. */\n headers?: Record<string, string>;\n /** optional log sink; never receives secrets. defaults to a console sink when a level is active. */\n logger?: QbrixLogger;\n /** logging verbosity. defaults to \"off\" (silent); also read from QBRIX_LOG / QBRIX_DEBUG. */\n logLevel?: LogLevel;\n}\n\nexport function buildHeaders(config: ResolvedConfig): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n };\n if (config.apiKey) {\n headers[\"X-API-Key\"] = config.apiKey;\n }\n // browsers forbid setting User-Agent and drop or error on the attempt\n if (typeof document === \"undefined\") {\n headers[\"User-Agent\"] = `qbrix-js/${VERSION}`;\n }\n return { ...headers, ...config.headers };\n}\n\nexport class QbrixClient {\n readonly config: ResolvedConfig;\n\n constructor(options: QbrixClientOptions = {}) {\n this.config = resolveConfig(options);\n }\n\n async select(experimentId: string, context: Context): Promise<SelectResult> {\n const body = toSelectRequest({ experimentId, context });\n const wire = await request<components[\"schemas\"][\"AgentSelectResponse\"]>(\n this.config,\n \"POST\",\n \"/api/v1/agent/select\",\n { body },\n );\n if (wire === undefined) {\n throw new QbrixError(\"qbrix: select returned no response body\");\n }\n return fromSelectResponse(wire);\n }\n\n async feedback(requestId: string, reward: number): Promise<void> {\n const body = toFeedbackRequest({ requestId, reward });\n await request(this.config, \"POST\", \"/api/v1/agent/feedback\", { body });\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
type LogLevel = "debug" | "info" | "warn" | "error" | "off";
|
|
2
|
+
interface QbrixLogger {
|
|
3
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
4
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
5
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
6
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ResolvedConfig {
|
|
10
|
+
apiKey: string | undefined;
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
timeout: number;
|
|
13
|
+
maxRetries: number;
|
|
14
|
+
retryOn: number[];
|
|
15
|
+
fetch: typeof fetch | undefined;
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
logger: QbrixLogger | undefined;
|
|
18
|
+
logLevel: LogLevel;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ErrorCode = "INTERNAL_ERROR" | "BAD_REQUEST" | "FEEDBACK_FAILED" | "INVALID_POLICY_PARAMS" | "UNAUTHORIZED" | "INVALID_TOKEN" | "INVALID_API_KEY" | "FORBIDDEN" | "INSUFFICIENT_SCOPES" | "PLAN_TIER_REQUIRED" | "LEARNER_EXPERIMENT_DELETE_FORBIDDEN" | "NOT_FOUND" | "POOL_NOT_FOUND" | "EXPERIMENT_NOT_FOUND" | "USER_NOT_FOUND" | "GATE_NOT_FOUND" | "CONFLICT" | "USER_ALREADY_EXISTS" | "API_KEY_LIMIT_REACHED" | "EXPERIMENT_LIMIT_REACHED" | "POOL_HAS_EXPERIMENTS" | "RATE_LIMITED" | "POOL_CREATION_FAILED" | "EXPERIMENT_CREATION_FAILED" | "SELECTION_FAILED" | "SERVICE_UNAVAILABLE";
|
|
22
|
+
interface Context {
|
|
23
|
+
id: string;
|
|
24
|
+
vector?: number[];
|
|
25
|
+
metadata?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
interface Arm {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
index: number;
|
|
31
|
+
}
|
|
32
|
+
interface SelectParams {
|
|
33
|
+
experimentId: string;
|
|
34
|
+
context: Context;
|
|
35
|
+
}
|
|
36
|
+
interface SelectResult {
|
|
37
|
+
arm: Arm;
|
|
38
|
+
requestId: string;
|
|
39
|
+
isDefault: boolean;
|
|
40
|
+
}
|
|
41
|
+
interface FeedbackParams {
|
|
42
|
+
requestId: string;
|
|
43
|
+
reward: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface QbrixClientOptions {
|
|
47
|
+
/** qbrix api key (prefix `optiq_`). falls back to `QBRIX_API_KEY`. */
|
|
48
|
+
apiKey?: string;
|
|
49
|
+
/** base url of the qbrix proxy. falls back to `QBRIX_BASE_URL`. */
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
/** request timeout in milliseconds. */
|
|
52
|
+
timeout?: number;
|
|
53
|
+
/** max retry attempts on retryable status codes. */
|
|
54
|
+
maxRetries?: number;
|
|
55
|
+
/** status codes to retry. */
|
|
56
|
+
retryOn?: number[];
|
|
57
|
+
/** custom fetch implementation, injectable for tests and custom runtimes. */
|
|
58
|
+
fetch?: typeof fetch;
|
|
59
|
+
/** extra headers merged into every request; user headers override the defaults. */
|
|
60
|
+
headers?: Record<string, string>;
|
|
61
|
+
/** optional log sink; never receives secrets. defaults to a console sink when a level is active. */
|
|
62
|
+
logger?: QbrixLogger;
|
|
63
|
+
/** logging verbosity. defaults to "off" (silent); also read from QBRIX_LOG / QBRIX_DEBUG. */
|
|
64
|
+
logLevel?: LogLevel;
|
|
65
|
+
}
|
|
66
|
+
declare class QbrixClient {
|
|
67
|
+
readonly config: ResolvedConfig;
|
|
68
|
+
constructor(options?: QbrixClientOptions);
|
|
69
|
+
select(experimentId: string, context: Context): Promise<SelectResult>;
|
|
70
|
+
feedback(requestId: string, reward: number): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
declare class QbrixError extends Error {
|
|
74
|
+
constructor(message: string);
|
|
75
|
+
}
|
|
76
|
+
interface ApiErrorOptions {
|
|
77
|
+
code?: ErrorCode | (string & {});
|
|
78
|
+
context?: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
declare class QbrixAPIError extends QbrixError {
|
|
81
|
+
readonly status: number;
|
|
82
|
+
readonly detail: string;
|
|
83
|
+
readonly code: ErrorCode | (string & {}) | undefined;
|
|
84
|
+
readonly context: Record<string, unknown> | undefined;
|
|
85
|
+
constructor(status: number, detail: string, options?: ApiErrorOptions);
|
|
86
|
+
}
|
|
87
|
+
declare class BadRequestError extends QbrixAPIError {
|
|
88
|
+
}
|
|
89
|
+
declare class AuthenticationError extends QbrixAPIError {
|
|
90
|
+
}
|
|
91
|
+
declare class ForbiddenError extends QbrixAPIError {
|
|
92
|
+
}
|
|
93
|
+
declare class NotFoundError extends QbrixAPIError {
|
|
94
|
+
}
|
|
95
|
+
declare class ConflictError extends QbrixAPIError {
|
|
96
|
+
}
|
|
97
|
+
interface RateLimitedErrorOptions extends ApiErrorOptions {
|
|
98
|
+
retryAfter?: number;
|
|
99
|
+
}
|
|
100
|
+
declare class RateLimitedError extends QbrixAPIError {
|
|
101
|
+
readonly retryAfter: number | undefined;
|
|
102
|
+
constructor(status: number, detail: string, options?: RateLimitedErrorOptions);
|
|
103
|
+
}
|
|
104
|
+
declare class InternalServerError extends QbrixAPIError {
|
|
105
|
+
}
|
|
106
|
+
declare class BadGatewayError extends QbrixAPIError {
|
|
107
|
+
}
|
|
108
|
+
declare class ServiceUnavailableError extends QbrixAPIError {
|
|
109
|
+
}
|
|
110
|
+
declare class GatewayTimeoutError extends QbrixAPIError {
|
|
111
|
+
}
|
|
112
|
+
declare class QbrixConnectionError extends QbrixError {
|
|
113
|
+
}
|
|
114
|
+
declare class QbrixTimeoutError extends QbrixError {
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { type Arm, AuthenticationError, BadGatewayError, BadRequestError, ConflictError, type Context, type ErrorCode, type FeedbackParams, ForbiddenError, GatewayTimeoutError, InternalServerError, type LogLevel, NotFoundError, QbrixAPIError, QbrixClient, type QbrixClientOptions, QbrixConnectionError, QbrixError, type QbrixLogger, QbrixTimeoutError, RateLimitedError, type SelectParams, type SelectResult, ServiceUnavailableError };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
type LogLevel = "debug" | "info" | "warn" | "error" | "off";
|
|
2
|
+
interface QbrixLogger {
|
|
3
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
4
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
5
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
6
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ResolvedConfig {
|
|
10
|
+
apiKey: string | undefined;
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
timeout: number;
|
|
13
|
+
maxRetries: number;
|
|
14
|
+
retryOn: number[];
|
|
15
|
+
fetch: typeof fetch | undefined;
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
logger: QbrixLogger | undefined;
|
|
18
|
+
logLevel: LogLevel;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ErrorCode = "INTERNAL_ERROR" | "BAD_REQUEST" | "FEEDBACK_FAILED" | "INVALID_POLICY_PARAMS" | "UNAUTHORIZED" | "INVALID_TOKEN" | "INVALID_API_KEY" | "FORBIDDEN" | "INSUFFICIENT_SCOPES" | "PLAN_TIER_REQUIRED" | "LEARNER_EXPERIMENT_DELETE_FORBIDDEN" | "NOT_FOUND" | "POOL_NOT_FOUND" | "EXPERIMENT_NOT_FOUND" | "USER_NOT_FOUND" | "GATE_NOT_FOUND" | "CONFLICT" | "USER_ALREADY_EXISTS" | "API_KEY_LIMIT_REACHED" | "EXPERIMENT_LIMIT_REACHED" | "POOL_HAS_EXPERIMENTS" | "RATE_LIMITED" | "POOL_CREATION_FAILED" | "EXPERIMENT_CREATION_FAILED" | "SELECTION_FAILED" | "SERVICE_UNAVAILABLE";
|
|
22
|
+
interface Context {
|
|
23
|
+
id: string;
|
|
24
|
+
vector?: number[];
|
|
25
|
+
metadata?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
interface Arm {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
index: number;
|
|
31
|
+
}
|
|
32
|
+
interface SelectParams {
|
|
33
|
+
experimentId: string;
|
|
34
|
+
context: Context;
|
|
35
|
+
}
|
|
36
|
+
interface SelectResult {
|
|
37
|
+
arm: Arm;
|
|
38
|
+
requestId: string;
|
|
39
|
+
isDefault: boolean;
|
|
40
|
+
}
|
|
41
|
+
interface FeedbackParams {
|
|
42
|
+
requestId: string;
|
|
43
|
+
reward: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface QbrixClientOptions {
|
|
47
|
+
/** qbrix api key (prefix `optiq_`). falls back to `QBRIX_API_KEY`. */
|
|
48
|
+
apiKey?: string;
|
|
49
|
+
/** base url of the qbrix proxy. falls back to `QBRIX_BASE_URL`. */
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
/** request timeout in milliseconds. */
|
|
52
|
+
timeout?: number;
|
|
53
|
+
/** max retry attempts on retryable status codes. */
|
|
54
|
+
maxRetries?: number;
|
|
55
|
+
/** status codes to retry. */
|
|
56
|
+
retryOn?: number[];
|
|
57
|
+
/** custom fetch implementation, injectable for tests and custom runtimes. */
|
|
58
|
+
fetch?: typeof fetch;
|
|
59
|
+
/** extra headers merged into every request; user headers override the defaults. */
|
|
60
|
+
headers?: Record<string, string>;
|
|
61
|
+
/** optional log sink; never receives secrets. defaults to a console sink when a level is active. */
|
|
62
|
+
logger?: QbrixLogger;
|
|
63
|
+
/** logging verbosity. defaults to "off" (silent); also read from QBRIX_LOG / QBRIX_DEBUG. */
|
|
64
|
+
logLevel?: LogLevel;
|
|
65
|
+
}
|
|
66
|
+
declare class QbrixClient {
|
|
67
|
+
readonly config: ResolvedConfig;
|
|
68
|
+
constructor(options?: QbrixClientOptions);
|
|
69
|
+
select(experimentId: string, context: Context): Promise<SelectResult>;
|
|
70
|
+
feedback(requestId: string, reward: number): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
declare class QbrixError extends Error {
|
|
74
|
+
constructor(message: string);
|
|
75
|
+
}
|
|
76
|
+
interface ApiErrorOptions {
|
|
77
|
+
code?: ErrorCode | (string & {});
|
|
78
|
+
context?: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
declare class QbrixAPIError extends QbrixError {
|
|
81
|
+
readonly status: number;
|
|
82
|
+
readonly detail: string;
|
|
83
|
+
readonly code: ErrorCode | (string & {}) | undefined;
|
|
84
|
+
readonly context: Record<string, unknown> | undefined;
|
|
85
|
+
constructor(status: number, detail: string, options?: ApiErrorOptions);
|
|
86
|
+
}
|
|
87
|
+
declare class BadRequestError extends QbrixAPIError {
|
|
88
|
+
}
|
|
89
|
+
declare class AuthenticationError extends QbrixAPIError {
|
|
90
|
+
}
|
|
91
|
+
declare class ForbiddenError extends QbrixAPIError {
|
|
92
|
+
}
|
|
93
|
+
declare class NotFoundError extends QbrixAPIError {
|
|
94
|
+
}
|
|
95
|
+
declare class ConflictError extends QbrixAPIError {
|
|
96
|
+
}
|
|
97
|
+
interface RateLimitedErrorOptions extends ApiErrorOptions {
|
|
98
|
+
retryAfter?: number;
|
|
99
|
+
}
|
|
100
|
+
declare class RateLimitedError extends QbrixAPIError {
|
|
101
|
+
readonly retryAfter: number | undefined;
|
|
102
|
+
constructor(status: number, detail: string, options?: RateLimitedErrorOptions);
|
|
103
|
+
}
|
|
104
|
+
declare class InternalServerError extends QbrixAPIError {
|
|
105
|
+
}
|
|
106
|
+
declare class BadGatewayError extends QbrixAPIError {
|
|
107
|
+
}
|
|
108
|
+
declare class ServiceUnavailableError extends QbrixAPIError {
|
|
109
|
+
}
|
|
110
|
+
declare class GatewayTimeoutError extends QbrixAPIError {
|
|
111
|
+
}
|
|
112
|
+
declare class QbrixConnectionError extends QbrixError {
|
|
113
|
+
}
|
|
114
|
+
declare class QbrixTimeoutError extends QbrixError {
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { type Arm, AuthenticationError, BadGatewayError, BadRequestError, ConflictError, type Context, type ErrorCode, type FeedbackParams, ForbiddenError, GatewayTimeoutError, InternalServerError, type LogLevel, NotFoundError, QbrixAPIError, QbrixClient, type QbrixClientOptions, QbrixConnectionError, QbrixError, type QbrixLogger, QbrixTimeoutError, RateLimitedError, type SelectParams, type SelectResult, ServiceUnavailableError };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// src/logger.ts
|
|
2
|
+
var RANK = {
|
|
3
|
+
debug: 10,
|
|
4
|
+
info: 20,
|
|
5
|
+
warn: 30,
|
|
6
|
+
error: 40,
|
|
7
|
+
off: 100
|
|
8
|
+
};
|
|
9
|
+
function shouldLog(configured, event) {
|
|
10
|
+
return RANK[event] >= RANK[configured];
|
|
11
|
+
}
|
|
12
|
+
var consoleLogger = {
|
|
13
|
+
debug: (message, context) => context ? console.debug(message, context) : console.debug(message),
|
|
14
|
+
info: (message, context) => context ? console.info(message, context) : console.info(message),
|
|
15
|
+
warn: (message, context) => context ? console.warn(message, context) : console.warn(message),
|
|
16
|
+
error: (message, context) => context ? console.error(message, context) : console.error(message)
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/config.ts
|
|
20
|
+
var LOG_LEVELS = ["debug", "info", "warn", "error", "off"];
|
|
21
|
+
function resolveLogLevel(option, hasLogger) {
|
|
22
|
+
if (option) return option;
|
|
23
|
+
const env = readEnv("QBRIX_LOG");
|
|
24
|
+
if (env && LOG_LEVELS.includes(env)) return env;
|
|
25
|
+
if (readEnv("QBRIX_DEBUG")) return "debug";
|
|
26
|
+
return hasLogger ? "debug" : "off";
|
|
27
|
+
}
|
|
28
|
+
var DEFAULTS = {
|
|
29
|
+
baseUrl: "http://localhost:8080",
|
|
30
|
+
timeout: 3e4,
|
|
31
|
+
maxRetries: 2,
|
|
32
|
+
retryOn: [429, 502, 503, 504]
|
|
33
|
+
};
|
|
34
|
+
function readEnv(name) {
|
|
35
|
+
try {
|
|
36
|
+
const env = globalThis.process?.env;
|
|
37
|
+
return env?.[name];
|
|
38
|
+
} catch {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function resolveConfig(options = {}) {
|
|
43
|
+
const logLevel = resolveLogLevel(options.logLevel, options.logger !== void 0);
|
|
44
|
+
const config = {
|
|
45
|
+
apiKey: options.apiKey ?? readEnv("QBRIX_API_KEY"),
|
|
46
|
+
baseUrl: options.baseUrl ?? readEnv("QBRIX_BASE_URL") ?? DEFAULTS.baseUrl,
|
|
47
|
+
timeout: options.timeout ?? DEFAULTS.timeout,
|
|
48
|
+
maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
|
|
49
|
+
retryOn: options.retryOn ?? [...DEFAULTS.retryOn],
|
|
50
|
+
fetch: options.fetch,
|
|
51
|
+
headers: options.headers ?? {},
|
|
52
|
+
logger: options.logger ?? (logLevel === "off" ? void 0 : consoleLogger),
|
|
53
|
+
logLevel
|
|
54
|
+
};
|
|
55
|
+
if (config.timeout <= 0) {
|
|
56
|
+
throw new Error(`qbrix: timeout must be > 0, got ${config.timeout}`);
|
|
57
|
+
}
|
|
58
|
+
if (config.maxRetries < 0) {
|
|
59
|
+
throw new Error(`qbrix: maxRetries must be >= 0, got ${config.maxRetries}`);
|
|
60
|
+
}
|
|
61
|
+
return config;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/errors.ts
|
|
65
|
+
var QbrixError = class extends Error {
|
|
66
|
+
constructor(message) {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = new.target.name;
|
|
69
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var QbrixAPIError = class extends QbrixError {
|
|
73
|
+
status;
|
|
74
|
+
detail;
|
|
75
|
+
code;
|
|
76
|
+
context;
|
|
77
|
+
constructor(status, detail, options) {
|
|
78
|
+
super(`[${status}] ${detail}`);
|
|
79
|
+
this.status = status;
|
|
80
|
+
this.detail = detail;
|
|
81
|
+
this.code = options?.code;
|
|
82
|
+
this.context = options?.context;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var BadRequestError = class extends QbrixAPIError {
|
|
86
|
+
};
|
|
87
|
+
var AuthenticationError = class extends QbrixAPIError {
|
|
88
|
+
};
|
|
89
|
+
var ForbiddenError = class extends QbrixAPIError {
|
|
90
|
+
};
|
|
91
|
+
var NotFoundError = class extends QbrixAPIError {
|
|
92
|
+
};
|
|
93
|
+
var ConflictError = class extends QbrixAPIError {
|
|
94
|
+
};
|
|
95
|
+
var RateLimitedError = class extends QbrixAPIError {
|
|
96
|
+
retryAfter;
|
|
97
|
+
constructor(status, detail, options) {
|
|
98
|
+
super(status, detail, options);
|
|
99
|
+
this.retryAfter = options?.retryAfter;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var InternalServerError = class extends QbrixAPIError {
|
|
103
|
+
};
|
|
104
|
+
var BadGatewayError = class extends QbrixAPIError {
|
|
105
|
+
};
|
|
106
|
+
var ServiceUnavailableError = class extends QbrixAPIError {
|
|
107
|
+
};
|
|
108
|
+
var GatewayTimeoutError = class extends QbrixAPIError {
|
|
109
|
+
};
|
|
110
|
+
var QbrixConnectionError = class extends QbrixError {
|
|
111
|
+
};
|
|
112
|
+
var QbrixTimeoutError = class extends QbrixError {
|
|
113
|
+
};
|
|
114
|
+
var STATUS_TO_ERROR = {
|
|
115
|
+
400: BadRequestError,
|
|
116
|
+
401: AuthenticationError,
|
|
117
|
+
403: ForbiddenError,
|
|
118
|
+
404: NotFoundError,
|
|
119
|
+
409: ConflictError,
|
|
120
|
+
429: RateLimitedError,
|
|
121
|
+
500: InternalServerError,
|
|
122
|
+
502: BadGatewayError,
|
|
123
|
+
503: ServiceUnavailableError,
|
|
124
|
+
504: GatewayTimeoutError
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/mapper.ts
|
|
128
|
+
function toSelectRequest(params) {
|
|
129
|
+
const { id, vector, metadata } = params.context;
|
|
130
|
+
return {
|
|
131
|
+
experiment_id: params.experimentId,
|
|
132
|
+
context: {
|
|
133
|
+
id,
|
|
134
|
+
...vector !== void 0 && { vector },
|
|
135
|
+
...metadata !== void 0 && { metadata }
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function fromSelectResponse(wire) {
|
|
140
|
+
return {
|
|
141
|
+
arm: { id: wire.arm.id, name: wire.arm.name, index: wire.arm.index },
|
|
142
|
+
requestId: wire.request_id,
|
|
143
|
+
isDefault: wire.is_default
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function toFeedbackRequest(params) {
|
|
147
|
+
return {
|
|
148
|
+
request_id: params.requestId,
|
|
149
|
+
reward: params.reward
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/transport.ts
|
|
154
|
+
var RETRY_BASE_DELAY_MS = 500;
|
|
155
|
+
var RETRY_MAX_DELAY_MS = 8e3;
|
|
156
|
+
function emit(config, level, message, context) {
|
|
157
|
+
if (config.logger && shouldLog(config.logLevel, level)) {
|
|
158
|
+
config.logger[level](message, context);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function request(config, method, path, options = {}) {
|
|
162
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
163
|
+
const url = joinUrl(config.baseUrl, path);
|
|
164
|
+
const headers = buildHeaders(config);
|
|
165
|
+
const body = options.body === void 0 ? void 0 : JSON.stringify(options.body);
|
|
166
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
167
|
+
emit(config, "debug", "request attempt", {
|
|
168
|
+
method,
|
|
169
|
+
path,
|
|
170
|
+
attempt: attempt + 1,
|
|
171
|
+
attempts: config.maxRetries + 1
|
|
172
|
+
});
|
|
173
|
+
const timeoutSignal = AbortSignal.timeout(config.timeout);
|
|
174
|
+
const signal = combineSignals(options.signal, timeoutSignal);
|
|
175
|
+
let response;
|
|
176
|
+
try {
|
|
177
|
+
response = await fetchImpl(url, { method, headers, body, signal });
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (options.signal?.aborted) throw options.signal.reason ?? err;
|
|
180
|
+
if (timeoutSignal.aborted) {
|
|
181
|
+
emit(config, "error", "request timed out", { method, path });
|
|
182
|
+
throw new QbrixTimeoutError(`qbrix: request timed out after ${config.timeout}ms`);
|
|
183
|
+
}
|
|
184
|
+
emit(config, "error", "request connection error", { method, path });
|
|
185
|
+
throw new QbrixConnectionError(err instanceof Error ? err.message : String(err));
|
|
186
|
+
}
|
|
187
|
+
if (response.ok) {
|
|
188
|
+
emit(config, "debug", "request succeeded", { method, path, status: response.status });
|
|
189
|
+
if (response.status === 204) return void 0;
|
|
190
|
+
const text = await response.text();
|
|
191
|
+
return text ? JSON.parse(text) : void 0;
|
|
192
|
+
}
|
|
193
|
+
const error = await makeApiError(response);
|
|
194
|
+
if (!config.retryOn.includes(response.status) || attempt === config.maxRetries) {
|
|
195
|
+
emit(config, "error", "request failed", { method, path, status: response.status });
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
const delay = retryDelay(attempt, error);
|
|
199
|
+
emit(config, "warn", "request retrying", {
|
|
200
|
+
method,
|
|
201
|
+
path,
|
|
202
|
+
status: response.status,
|
|
203
|
+
attempt: attempt + 1,
|
|
204
|
+
delayMs: Math.round(delay)
|
|
205
|
+
});
|
|
206
|
+
await sleep(delay, options.signal);
|
|
207
|
+
}
|
|
208
|
+
throw new QbrixError("qbrix: request failed");
|
|
209
|
+
}
|
|
210
|
+
function joinUrl(baseUrl, path) {
|
|
211
|
+
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
212
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
213
|
+
return `${base}${suffix}`;
|
|
214
|
+
}
|
|
215
|
+
function combineSignals(caller, timeout) {
|
|
216
|
+
if (!caller) return timeout;
|
|
217
|
+
const anyFn = AbortSignal.any;
|
|
218
|
+
if (typeof anyFn === "function") return anyFn([caller, timeout]);
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
const forward = (source) => () => controller.abort(source.reason);
|
|
221
|
+
if (caller.aborted) controller.abort(caller.reason);
|
|
222
|
+
else if (timeout.aborted) controller.abort(timeout.reason);
|
|
223
|
+
else {
|
|
224
|
+
caller.addEventListener("abort", forward(caller), { once: true });
|
|
225
|
+
timeout.addEventListener("abort", forward(timeout), { once: true });
|
|
226
|
+
}
|
|
227
|
+
return controller.signal;
|
|
228
|
+
}
|
|
229
|
+
async function makeApiError(response) {
|
|
230
|
+
const raw = await response.text();
|
|
231
|
+
let detail = raw || response.statusText;
|
|
232
|
+
let code;
|
|
233
|
+
let context;
|
|
234
|
+
if (raw) {
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(raw);
|
|
237
|
+
if (typeof parsed.detail === "string") detail = parsed.detail;
|
|
238
|
+
if (typeof parsed.code === "string") code = parsed.code;
|
|
239
|
+
if (parsed.context && typeof parsed.context === "object") {
|
|
240
|
+
context = parsed.context;
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const status = response.status;
|
|
246
|
+
if (status === 429) {
|
|
247
|
+
const header = response.headers.get("Retry-After");
|
|
248
|
+
const seconds = header ? Number.parseFloat(header) : Number.NaN;
|
|
249
|
+
return new RateLimitedError(status, detail, {
|
|
250
|
+
code,
|
|
251
|
+
context,
|
|
252
|
+
retryAfter: Number.isFinite(seconds) ? seconds : void 0
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const ErrorClass = STATUS_TO_ERROR[status] ?? QbrixAPIError;
|
|
256
|
+
return new ErrorClass(status, detail, { code, context });
|
|
257
|
+
}
|
|
258
|
+
function retryDelay(attempt, error) {
|
|
259
|
+
if (error instanceof RateLimitedError && error.retryAfter !== void 0) {
|
|
260
|
+
return Math.min(error.retryAfter * 1e3, RETRY_MAX_DELAY_MS);
|
|
261
|
+
}
|
|
262
|
+
const base = Math.min(RETRY_BASE_DELAY_MS * 2 ** attempt, RETRY_MAX_DELAY_MS);
|
|
263
|
+
return base + Math.random() * base * 0.1;
|
|
264
|
+
}
|
|
265
|
+
function sleep(ms, signal) {
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
if (signal?.aborted) {
|
|
268
|
+
reject(signal.reason);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const timer = setTimeout(() => {
|
|
272
|
+
signal?.removeEventListener("abort", onAbort);
|
|
273
|
+
resolve();
|
|
274
|
+
}, ms);
|
|
275
|
+
function onAbort() {
|
|
276
|
+
clearTimeout(timer);
|
|
277
|
+
reject(signal?.reason);
|
|
278
|
+
}
|
|
279
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/version.ts
|
|
284
|
+
var VERSION = "0.0.0";
|
|
285
|
+
|
|
286
|
+
// src/client.ts
|
|
287
|
+
function buildHeaders(config) {
|
|
288
|
+
const headers = {
|
|
289
|
+
Accept: "application/json",
|
|
290
|
+
"Content-Type": "application/json"
|
|
291
|
+
};
|
|
292
|
+
if (config.apiKey) {
|
|
293
|
+
headers["X-API-Key"] = config.apiKey;
|
|
294
|
+
}
|
|
295
|
+
if (typeof document === "undefined") {
|
|
296
|
+
headers["User-Agent"] = `qbrix-js/${VERSION}`;
|
|
297
|
+
}
|
|
298
|
+
return { ...headers, ...config.headers };
|
|
299
|
+
}
|
|
300
|
+
var QbrixClient = class {
|
|
301
|
+
config;
|
|
302
|
+
constructor(options = {}) {
|
|
303
|
+
this.config = resolveConfig(options);
|
|
304
|
+
}
|
|
305
|
+
async select(experimentId, context) {
|
|
306
|
+
const body = toSelectRequest({ experimentId, context });
|
|
307
|
+
const wire = await request(
|
|
308
|
+
this.config,
|
|
309
|
+
"POST",
|
|
310
|
+
"/api/v1/agent/select",
|
|
311
|
+
{ body }
|
|
312
|
+
);
|
|
313
|
+
if (wire === void 0) {
|
|
314
|
+
throw new QbrixError("qbrix: select returned no response body");
|
|
315
|
+
}
|
|
316
|
+
return fromSelectResponse(wire);
|
|
317
|
+
}
|
|
318
|
+
async feedback(requestId, reward) {
|
|
319
|
+
const body = toFeedbackRequest({ requestId, reward });
|
|
320
|
+
await request(this.config, "POST", "/api/v1/agent/feedback", { body });
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export { AuthenticationError, BadGatewayError, BadRequestError, ConflictError, ForbiddenError, GatewayTimeoutError, InternalServerError, NotFoundError, QbrixAPIError, QbrixClient, QbrixConnectionError, QbrixError, QbrixTimeoutError, RateLimitedError, ServiceUnavailableError };
|
|
325
|
+
//# sourceMappingURL=index.js.map
|
|
326
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/logger.ts","../src/config.ts","../src/errors.ts","../src/mapper.ts","../src/transport.ts","../src/version.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// pluggable, leveled logging. the sdk is silent by default (logLevel \"off\") and\n// only emits when a level is configured (via the logLevel option, the QBRIX_LOG\n// / QBRIX_DEBUG env vars, or by injecting a logger). the sdk passes only\n// non-sensitive context (method, path, status, attempt counts) — never the api\n// key, headers, or request/response bodies.\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"off\";\n\nexport interface QbrixLogger {\n debug(message: string, context?: Record<string, unknown>): void;\n info(message: string, context?: Record<string, unknown>): void;\n warn(message: string, context?: Record<string, unknown>): void;\n error(message: string, context?: Record<string, unknown>): void;\n}\n\nconst RANK: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n off: 100,\n};\n\n// emit an event at `event` level only when it meets the configured threshold.\nexport function shouldLog(configured: LogLevel, event: Exclude<LogLevel, \"off\">): boolean {\n return RANK[event] >= RANK[configured];\n}\n\n// built-in sink for the env / logLevel opt-in; routes each level to console.\nexport const consoleLogger: QbrixLogger = {\n debug: (message, context) => (context ? console.debug(message, context) : console.debug(message)),\n info: (message, context) => (context ? console.info(message, context) : console.info(message)),\n warn: (message, context) => (context ? console.warn(message, context) : console.warn(message)),\n error: (message, context) => (context ? console.error(message, context) : console.error(message)),\n};\n","import type { QbrixClientOptions } from \"./client\";\nimport { type LogLevel, type QbrixLogger, consoleLogger } from \"./logger\";\n\nexport interface ResolvedConfig {\n apiKey: string | undefined;\n baseUrl: string;\n timeout: number;\n maxRetries: number;\n retryOn: number[];\n fetch: typeof fetch | undefined;\n headers: Record<string, string>;\n logger: QbrixLogger | undefined;\n logLevel: LogLevel;\n}\n\nconst LOG_LEVELS: readonly LogLevel[] = [\"debug\", \"info\", \"warn\", \"error\", \"off\"];\n\n// precedence: explicit option → QBRIX_LOG → QBRIX_DEBUG (sugar for \"debug\") →\n// \"debug\" when a logger is injected (opt-in by passing one) → \"off\" (silent).\nfunction resolveLogLevel(option: LogLevel | undefined, hasLogger: boolean): LogLevel {\n if (option) return option;\n const env = readEnv(\"QBRIX_LOG\");\n if (env && (LOG_LEVELS as readonly string[]).includes(env)) return env as LogLevel;\n if (readEnv(\"QBRIX_DEBUG\")) return \"debug\";\n return hasLogger ? \"debug\" : \"off\";\n}\n\n// todo: check the defaults / might want to add the hosted url here / update the timeout to be low latency etc.\nconst DEFAULTS = {\n baseUrl: \"http://localhost:8080\",\n timeout: 30_000,\n maxRetries: 2,\n retryOn: [429, 502, 503, 504] as number[],\n};\n\n// guarded globalThis access keeps the browser bundle clean and avoids needing `@types/node`.\n// the try/catch matters for deno, where reading `process.env` throws without `--allow-env` —\n// the sdk treats env as unset rather than crashing.\nfunction readEnv(name: string): string | undefined {\n try {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n ?.env;\n return env?.[name];\n } catch {\n return undefined;\n }\n}\n\n// precedence per option: explicit arg → env var → default\nexport function resolveConfig(options: QbrixClientOptions = {}): ResolvedConfig {\n const logLevel = resolveLogLevel(options.logLevel, options.logger !== undefined);\n const config: ResolvedConfig = {\n apiKey: options.apiKey ?? readEnv(\"QBRIX_API_KEY\"),\n baseUrl: options.baseUrl ?? readEnv(\"QBRIX_BASE_URL\") ?? DEFAULTS.baseUrl,\n timeout: options.timeout ?? DEFAULTS.timeout,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryOn: options.retryOn ?? [...DEFAULTS.retryOn],\n fetch: options.fetch,\n headers: options.headers ?? {},\n logger: options.logger ?? (logLevel === \"off\" ? undefined : consoleLogger),\n logLevel,\n };\n\n if (config.timeout <= 0) {\n throw new Error(`qbrix: timeout must be > 0, got ${config.timeout}`);\n }\n if (config.maxRetries < 0) {\n throw new Error(`qbrix: maxRetries must be >= 0, got ${config.maxRetries}`);\n }\n\n return config;\n}\n","import type { ErrorCode } from \"./types\";\n\nexport class QbrixError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n // keep instanceof working when consumers downlevel below es2015\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// `string & {}` keeps autocomplete of the known codes while tolerating codes a\n// newer backend may send before the sdk's generated union catches up.\ninterface ApiErrorOptions {\n code?: ErrorCode | (string & {});\n context?: Record<string, unknown>;\n}\n\nexport class QbrixAPIError extends QbrixError {\n readonly status: number;\n readonly detail: string;\n readonly code: ErrorCode | (string & {}) | undefined;\n readonly context: Record<string, unknown> | undefined;\n\n constructor(status: number, detail: string, options?: ApiErrorOptions) {\n super(`[${status}] ${detail}`);\n this.status = status;\n this.detail = detail;\n this.code = options?.code;\n this.context = options?.context;\n }\n}\n\nexport class BadRequestError extends QbrixAPIError {}\nexport class AuthenticationError extends QbrixAPIError {}\nexport class ForbiddenError extends QbrixAPIError {}\nexport class NotFoundError extends QbrixAPIError {}\nexport class ConflictError extends QbrixAPIError {}\n\ninterface RateLimitedErrorOptions extends ApiErrorOptions {\n retryAfter?: number;\n}\n\nexport class RateLimitedError extends QbrixAPIError {\n readonly retryAfter: number | undefined;\n\n constructor(status: number, detail: string, options?: RateLimitedErrorOptions) {\n super(status, detail, options);\n this.retryAfter = options?.retryAfter;\n }\n}\n\nexport class InternalServerError extends QbrixAPIError {}\nexport class BadGatewayError extends QbrixAPIError {}\nexport class ServiceUnavailableError extends QbrixAPIError {}\nexport class GatewayTimeoutError extends QbrixAPIError {}\n\nexport class QbrixConnectionError extends QbrixError {}\nexport class QbrixTimeoutError extends QbrixError {}\n\ntype ApiErrorConstructor = new (\n status: number,\n detail: string,\n options?: ApiErrorOptions,\n) => QbrixAPIError;\n\nexport const STATUS_TO_ERROR: Record<number, ApiErrorConstructor> = {\n 400: BadRequestError,\n 401: AuthenticationError,\n 403: ForbiddenError,\n 404: NotFoundError,\n 409: ConflictError,\n 429: RateLimitedError,\n 500: InternalServerError,\n 502: BadGatewayError,\n 503: ServiceUnavailableError,\n 504: GatewayTimeoutError,\n};\n","import type { components } from \"./generated\";\nimport type { FeedbackParams, SelectParams, SelectResult } from \"./types\";\n\ntype WireSelectRequest = components[\"schemas\"][\"AgentSelectRequest\"];\ntype WireSelectResponse = components[\"schemas\"][\"AgentSelectResponse\"];\ntype WireFeedbackRequest = components[\"schemas\"][\"AgentFeedbackRequest\"];\n\nexport function toSelectRequest(params: SelectParams): WireSelectRequest {\n const { id, vector, metadata } = params.context;\n return {\n experiment_id: params.experimentId,\n context: {\n id,\n ...(vector !== undefined && { vector }),\n ...(metadata !== undefined && { metadata }),\n },\n };\n}\n\nexport function fromSelectResponse(wire: WireSelectResponse): SelectResult {\n return {\n arm: { id: wire.arm.id, name: wire.arm.name, index: wire.arm.index },\n requestId: wire.request_id,\n isDefault: wire.is_default,\n };\n}\n\nexport function toFeedbackRequest(params: FeedbackParams): WireFeedbackRequest {\n return {\n request_id: params.requestId,\n reward: params.reward,\n };\n}\n","import { buildHeaders } from \"./client\";\nimport type { ResolvedConfig } from \"./config\";\nimport {\n QbrixAPIError,\n QbrixConnectionError,\n QbrixError,\n QbrixTimeoutError,\n RateLimitedError,\n STATUS_TO_ERROR,\n} from \"./errors\";\nimport type { components } from \"./generated\";\nimport { type LogLevel, shouldLog } from \"./logger\";\n\nexport interface RequestOptions {\n body?: unknown;\n signal?: AbortSignal;\n}\n\nconst RETRY_BASE_DELAY_MS = 500;\nconst RETRY_MAX_DELAY_MS = 8_000;\n\nfunction emit(\n config: ResolvedConfig,\n level: Exclude<LogLevel, \"off\">,\n message: string,\n context: Record<string, unknown>,\n): void {\n if (config.logger && shouldLog(config.logLevel, level)) {\n config.logger[level](message, context);\n }\n}\n\nexport async function request<T>(\n config: ResolvedConfig,\n method: string,\n path: string,\n options: RequestOptions = {},\n): Promise<T | undefined> {\n const fetchImpl = config.fetch ?? globalThis.fetch;\n const url = joinUrl(config.baseUrl, path);\n const headers = buildHeaders(config);\n const body = options.body === undefined ? undefined : JSON.stringify(options.body);\n\n for (let attempt = 0; attempt <= config.maxRetries; attempt++) {\n emit(config, \"debug\", \"request attempt\", {\n method,\n path,\n attempt: attempt + 1,\n attempts: config.maxRetries + 1,\n });\n const timeoutSignal = AbortSignal.timeout(config.timeout);\n const signal = combineSignals(options.signal, timeoutSignal);\n\n let response: Response;\n try {\n response = await fetchImpl(url, { method, headers, body, signal });\n } catch (err) {\n // caller-initiated abort takes precedence and propagates unchanged\n if (options.signal?.aborted) throw options.signal.reason ?? err;\n if (timeoutSignal.aborted) {\n emit(config, \"error\", \"request timed out\", { method, path });\n throw new QbrixTimeoutError(`qbrix: request timed out after ${config.timeout}ms`);\n }\n emit(config, \"error\", \"request connection error\", { method, path });\n throw new QbrixConnectionError(err instanceof Error ? err.message : String(err));\n }\n\n if (response.ok) {\n emit(config, \"debug\", \"request succeeded\", { method, path, status: response.status });\n if (response.status === 204) return undefined;\n const text = await response.text();\n return text ? (JSON.parse(text) as T) : undefined;\n }\n\n const error = await makeApiError(response);\n if (!config.retryOn.includes(response.status) || attempt === config.maxRetries) {\n emit(config, \"error\", \"request failed\", { method, path, status: response.status });\n throw error;\n }\n const delay = retryDelay(attempt, error);\n emit(config, \"warn\", \"request retrying\", {\n method,\n path,\n status: response.status,\n attempt: attempt + 1,\n delayMs: Math.round(delay),\n });\n await sleep(delay, options.signal);\n }\n\n // unreachable: the final attempt always returns or throws\n throw new QbrixError(\"qbrix: request failed\");\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n const base = baseUrl.endsWith(\"/\") ? baseUrl.slice(0, -1) : baseUrl;\n const suffix = path.startsWith(\"/\") ? path : `/${path}`;\n return `${base}${suffix}`;\n}\n\nfunction combineSignals(caller: AbortSignal | undefined, timeout: AbortSignal): AbortSignal {\n if (!caller) return timeout;\n const anyFn = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;\n if (typeof anyFn === \"function\") return anyFn([caller, timeout]);\n\n // manual fallback for runtimes without AbortSignal.any\n const controller = new AbortController();\n const forward = (source: AbortSignal) => () => controller.abort(source.reason);\n if (caller.aborted) controller.abort(caller.reason);\n else if (timeout.aborted) controller.abort(timeout.reason);\n else {\n caller.addEventListener(\"abort\", forward(caller), { once: true });\n timeout.addEventListener(\"abort\", forward(timeout), { once: true });\n }\n return controller.signal;\n}\n\ntype WireError = components[\"schemas\"][\"ErrorResponse\"];\n\nasync function makeApiError(response: Response): Promise<QbrixAPIError> {\n // read the body once; fetch streams can't be re-read after JSON.parse fails\n const raw = await response.text();\n let detail = raw || response.statusText;\n let code: WireError[\"code\"] | undefined;\n let context: Record<string, unknown> | undefined;\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Partial<WireError>;\n if (typeof parsed.detail === \"string\") detail = parsed.detail;\n if (typeof parsed.code === \"string\") code = parsed.code;\n if (parsed.context && typeof parsed.context === \"object\") {\n context = parsed.context as Record<string, unknown>;\n }\n } catch {\n // non-json body — keep the raw text as detail\n }\n }\n\n const status = response.status;\n if (status === 429) {\n const header = response.headers.get(\"Retry-After\");\n const seconds = header ? Number.parseFloat(header) : Number.NaN;\n return new RateLimitedError(status, detail, {\n code,\n context,\n retryAfter: Number.isFinite(seconds) ? seconds : undefined,\n });\n }\n\n const ErrorClass = STATUS_TO_ERROR[status] ?? QbrixAPIError;\n return new ErrorClass(status, detail, { code, context });\n}\n\nfunction retryDelay(attempt: number, error: QbrixAPIError): number {\n if (error instanceof RateLimitedError && error.retryAfter !== undefined) {\n return Math.min(error.retryAfter * 1000, RETRY_MAX_DELAY_MS);\n }\n const base = Math.min(RETRY_BASE_DELAY_MS * 2 ** attempt, RETRY_MAX_DELAY_MS);\n return base + Math.random() * base * 0.1;\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(signal.reason);\n return;\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n function onAbort() {\n clearTimeout(timer);\n reject(signal?.reason);\n }\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n","// kept in sync with package.json by release tooling (OPT-41).\nexport const VERSION = \"0.0.0\";\n","import type { ResolvedConfig } from \"./config\";\nimport { resolveConfig } from \"./config\";\nimport { QbrixError } from \"./errors\";\nimport type { components } from \"./generated\";\nimport type { LogLevel, QbrixLogger } from \"./logger\";\nimport { fromSelectResponse, toFeedbackRequest, toSelectRequest } from \"./mapper\";\nimport { request } from \"./transport\";\nimport type { Context, SelectResult } from \"./types\";\nimport { VERSION } from \"./version\";\n\nexport interface QbrixClientOptions {\n /** qbrix api key (prefix `optiq_`). falls back to `QBRIX_API_KEY`. */\n apiKey?: string;\n /** base url of the qbrix proxy. falls back to `QBRIX_BASE_URL`. */\n baseUrl?: string;\n /** request timeout in milliseconds. */\n timeout?: number;\n /** max retry attempts on retryable status codes. */\n maxRetries?: number;\n /** status codes to retry. */\n retryOn?: number[];\n /** custom fetch implementation, injectable for tests and custom runtimes. */\n fetch?: typeof fetch;\n /** extra headers merged into every request; user headers override the defaults. */\n headers?: Record<string, string>;\n /** optional log sink; never receives secrets. defaults to a console sink when a level is active. */\n logger?: QbrixLogger;\n /** logging verbosity. defaults to \"off\" (silent); also read from QBRIX_LOG / QBRIX_DEBUG. */\n logLevel?: LogLevel;\n}\n\nexport function buildHeaders(config: ResolvedConfig): Record<string, string> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n };\n if (config.apiKey) {\n headers[\"X-API-Key\"] = config.apiKey;\n }\n // browsers forbid setting User-Agent and drop or error on the attempt\n if (typeof document === \"undefined\") {\n headers[\"User-Agent\"] = `qbrix-js/${VERSION}`;\n }\n return { ...headers, ...config.headers };\n}\n\nexport class QbrixClient {\n readonly config: ResolvedConfig;\n\n constructor(options: QbrixClientOptions = {}) {\n this.config = resolveConfig(options);\n }\n\n async select(experimentId: string, context: Context): Promise<SelectResult> {\n const body = toSelectRequest({ experimentId, context });\n const wire = await request<components[\"schemas\"][\"AgentSelectResponse\"]>(\n this.config,\n \"POST\",\n \"/api/v1/agent/select\",\n { body },\n );\n if (wire === undefined) {\n throw new QbrixError(\"qbrix: select returned no response body\");\n }\n return fromSelectResponse(wire);\n }\n\n async feedback(requestId: string, reward: number): Promise<void> {\n const body = toFeedbackRequest({ requestId, reward });\n await request(this.config, \"POST\", \"/api/v1/agent/feedback\", { body });\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@optiqio/qbrix",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JavaScript/TypeScript SDK for the Qbrix multi-armed bandit platform — select and feedback in ~10 lines.",
|
|
5
|
+
"keywords": ["bandit", "mab", "experimentation", "optimization", "ab-testing", "qbrix"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Optiq <hello@qbrix.io>",
|
|
8
|
+
"homepage": "https://qbrix.io",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/optiq-io/qbrix-js.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/optiq-io/qbrix-js/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"require": {
|
|
27
|
+
"types": "./dist/index.d.cts",
|
|
28
|
+
"default": "./dist/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": ["dist"],
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"provenance": true
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"dev": "tsup --watch",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"coverage": "vitest run --coverage",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"lint": "biome check .",
|
|
49
|
+
"format": "biome format --write .",
|
|
50
|
+
"spec:pull": "node scripts/pull-spec.mjs",
|
|
51
|
+
"generate": "openapi-typescript spec/proxysvc.openapi.json -o src/generated.ts",
|
|
52
|
+
"smoke": "node smoke/round-trip.mjs",
|
|
53
|
+
"smoke:bundle": "node scripts/check-browser-bundle.mjs",
|
|
54
|
+
"check:exports": "node scripts/check-package-exports.mjs",
|
|
55
|
+
"prepublishOnly": "npm run build",
|
|
56
|
+
"version": "changeset version && npm install --package-lock-only --ignore-scripts && biome format --write package.json",
|
|
57
|
+
"release": "changeset publish"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@biomejs/biome": "^1.9.4",
|
|
61
|
+
"@changesets/cli": "^2.27.11",
|
|
62
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
63
|
+
"esbuild": "^0.27.7",
|
|
64
|
+
"openapi-typescript": "^7.13.0",
|
|
65
|
+
"tsup": "^8.3.5",
|
|
66
|
+
"typescript": "^5.7.2",
|
|
67
|
+
"vitest": "^2.1.8"
|
|
68
|
+
}
|
|
69
|
+
}
|