@north-light/sdk 0.3.632 → 0.3.633
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.
|
@@ -36,7 +36,7 @@ export interface InferenceEndpointOptions {
|
|
|
36
36
|
baseURL: string;
|
|
37
37
|
/** The app's own principal, `app:<id>`; it prefixes the vendor reference of a call the vendor gave no request id. */
|
|
38
38
|
app: string;
|
|
39
|
-
/** The app's Anthropic API key. It
|
|
39
|
+
/** The app's Anthropic API key. It serves a call the directory admits as card-paid (no `pool` in the admission) and a Northlight-paid call the pool could not serve (see `inferenceEndpoint`). It is never logged or returned. */
|
|
40
40
|
apiKey: string;
|
|
41
41
|
/** Strength to model: `anthropic/<model id>` for each rung the endpoint serves. A request naming any other model is refused. Every model needs a list price in this SDK; the constructor throws for one that has none. */
|
|
42
42
|
rungs: Partial<Record<InferenceRung, string>>;
|
|
@@ -60,9 +60,15 @@ export interface InferenceEndpointOptions {
|
|
|
60
60
|
* directory's `payment` body, and a directory that cannot be reached or answers anything unexpected is 503 with nothing sent upstream.
|
|
61
61
|
*
|
|
62
62
|
* The upstream comes from the admission answer. An answer carrying `pool` (Northlight pays) sends the call to the pool at
|
|
63
|
-
* `pool.url` with `pool.token`, and
|
|
64
|
-
* the call to Anthropic with the app's `apiKey`, and `cost_usd` equals the list price.
|
|
65
|
-
*
|
|
63
|
+
* `pool.url` with `pool.token`, and a call the pool serves settles at list price with `cost_usd` 0. An answer without it (the person
|
|
64
|
+
* pays) sends the call to Anthropic with the app's `apiKey`, and `cost_usd` equals the list price.
|
|
65
|
+
*
|
|
66
|
+
* When the pool cannot serve a Northlight-paid call, the same request is sent to Anthropic with `apiKey` instead of being refused.
|
|
67
|
+
* That happens when the pool is unreachable or answers 401, 402, 403, 404, 408, 429 or any 5xx (every login rate-limited, no login,
|
|
68
|
+
* a pool-side refusal, a pool failure). Any other pool answer, notably a 400 for a malformed request, goes back to the caller
|
|
69
|
+
* unchanged, because the key would fail the same way. A call the key serves settles under the same admission with `cost_usd` equal
|
|
70
|
+
* to the list price (Northlight paid real money for it), so the directory counts it as it counts a pool call, and the log entry
|
|
71
|
+
* says `upstream: 'api_key_fallback'` with the pool's status. If the key's attempt fails too, its answer (or a 502) reaches the caller.
|
|
66
72
|
*
|
|
67
73
|
* The answer streams back with `Crtr-Charge-Id` and calculated `crtr_charge` on its terminal event (or JSON body), without waiting
|
|
68
74
|
* for settlement. Usage is read off the stream and reported to the directory at the list price of the model the vendor reports it
|
|
@@ -9,6 +9,9 @@ const DIRECTORY_TIMEOUT_MS = 10_000;
|
|
|
9
9
|
/** Posts made in the request's own lifetime before the stored report is left to `sweep`. */
|
|
10
10
|
const IMMEDIATE_ATTEMPTS = 3;
|
|
11
11
|
const SWEEP_BATCH = 50;
|
|
12
|
+
/** Pool answers the API key could plausibly serve instead: the pool or its login is unavailable or refused the call (401, 402, 403, 404 no login, 408), every login is rate-limited (429), or the pool failed (5xx). Any other status, 400 above all, would fail the same way at Anthropic and reaches the caller. */
|
|
13
|
+
const POOL_FALLBACK_STATUSES = new Set([401, 402, 403, 404, 408, 429]);
|
|
14
|
+
const poolFallbackStatus = (status) => POOL_FALLBACK_STATUSES.has(status) || status >= 500;
|
|
12
15
|
const STRIPPED_REQUEST_HEADERS = ['host', 'connection', 'keep-alive', 'transfer-encoding', 'te', 'upgrade', 'content-length', 'authorization', 'x-api-key', 'cookie', 'proxy-authorization'];
|
|
13
16
|
// The body is decoded and re-streamed, so these describe the upstream wire form, not what we send.
|
|
14
17
|
const STRIPPED_RESPONSE_HEADERS = ['connection', 'keep-alive', 'transfer-encoding', 'content-encoding', 'content-length'];
|
|
@@ -150,9 +153,15 @@ function chargedBody(body, contentType, charge) {
|
|
|
150
153
|
* directory's `payment` body, and a directory that cannot be reached or answers anything unexpected is 503 with nothing sent upstream.
|
|
151
154
|
*
|
|
152
155
|
* The upstream comes from the admission answer. An answer carrying `pool` (Northlight pays) sends the call to the pool at
|
|
153
|
-
* `pool.url` with `pool.token`, and
|
|
154
|
-
* the call to Anthropic with the app's `apiKey`, and `cost_usd` equals the list price.
|
|
155
|
-
*
|
|
156
|
+
* `pool.url` with `pool.token`, and a call the pool serves settles at list price with `cost_usd` 0. An answer without it (the person
|
|
157
|
+
* pays) sends the call to Anthropic with the app's `apiKey`, and `cost_usd` equals the list price.
|
|
158
|
+
*
|
|
159
|
+
* When the pool cannot serve a Northlight-paid call, the same request is sent to Anthropic with `apiKey` instead of being refused.
|
|
160
|
+
* That happens when the pool is unreachable or answers 401, 402, 403, 404, 408, 429 or any 5xx (every login rate-limited, no login,
|
|
161
|
+
* a pool-side refusal, a pool failure). Any other pool answer, notably a 400 for a malformed request, goes back to the caller
|
|
162
|
+
* unchanged, because the key would fail the same way. A call the key serves settles under the same admission with `cost_usd` equal
|
|
163
|
+
* to the list price (Northlight paid real money for it), so the directory counts it as it counts a pool call, and the log entry
|
|
164
|
+
* says `upstream: 'api_key_fallback'` with the pool's status. If the key's attempt fails too, its answer (or a 502) reaches the caller.
|
|
156
165
|
*
|
|
157
166
|
* The answer streams back with `Crtr-Charge-Id` and calculated `crtr_charge` on its terminal event (or JSON body), without waiting
|
|
158
167
|
* for settlement. Usage is read off the stream and reported to the directory at the list price of the model the vendor reports it
|
|
@@ -207,7 +216,7 @@ export function inferenceEndpoint(options) {
|
|
|
207
216
|
if (pool === undefined)
|
|
208
217
|
return { kind: 'admitted', admissionId: admission_id, chargeId: charge_id, pool: undefined };
|
|
209
218
|
const { url, token: poolToken } = object(pool) ?? {};
|
|
210
|
-
//
|
|
219
|
+
// An admission whose pool the helper cannot read is malformed, so the call is not served.
|
|
211
220
|
return typeof url === 'string' && url !== '' && typeof poolToken === 'string' && poolToken !== ''
|
|
212
221
|
? { kind: 'admitted', admissionId: admission_id, chargeId: charge_id, pool: { url: url.replace(/\/+$/, ''), token: poolToken } }
|
|
213
222
|
: { kind: 'unavailable' };
|
|
@@ -307,22 +316,47 @@ export function inferenceEndpoint(options) {
|
|
|
307
316
|
return refuse(403, 'permission_error', admission.message);
|
|
308
317
|
if (admission.kind === 'unavailable')
|
|
309
318
|
return refuse(503, 'admission_unavailable', 'Northlight could not confirm payment for this call. Try again shortly.');
|
|
310
|
-
const
|
|
319
|
+
const baseHeaders = new Headers(request.headers);
|
|
311
320
|
for (const name of STRIPPED_REQUEST_HEADERS)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
321
|
+
baseHeaders.delete(name);
|
|
322
|
+
// Not tied to the caller's connection: the vendor bills the whole answer, so the usage branch reads it to its end and the settlement is complete.
|
|
323
|
+
const sendToPool = (pool) => {
|
|
324
|
+
const headers = new Headers(baseHeaders);
|
|
315
325
|
headers.set('authorization', `Bearer ${pool.token}`);
|
|
316
|
-
|
|
326
|
+
return upstreamFetch(`${pool.url}/${POOL_MESSAGES_PATH}`, { method: 'POST', headers, body, redirect: 'manual' });
|
|
327
|
+
};
|
|
328
|
+
const sendToVendor = () => {
|
|
329
|
+
const headers = new Headers(baseHeaders);
|
|
317
330
|
headers.set('x-api-key', options.apiKey);
|
|
318
|
-
|
|
331
|
+
return upstreamFetch(`${VENDOR_URL}/${MESSAGES_PATH}`, { method: 'POST', headers, body, redirect: 'manual' });
|
|
332
|
+
};
|
|
319
333
|
let answer;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
334
|
+
let upstream = admission.pool !== undefined ? 'pool' : 'api_key';
|
|
335
|
+
if (admission.pool !== undefined) {
|
|
336
|
+
let poolFailure;
|
|
337
|
+
try {
|
|
338
|
+
answer = await sendToPool(admission.pool);
|
|
339
|
+
if (poolFallbackStatus(answer.status)) {
|
|
340
|
+
poolFailure = String(answer.status);
|
|
341
|
+
void answer.body?.cancel().catch(() => undefined);
|
|
342
|
+
answer = undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
poolFailure = String(error);
|
|
347
|
+
}
|
|
348
|
+
if (poolFailure !== undefined) {
|
|
349
|
+
upstream = 'api_key_fallback';
|
|
350
|
+
log({ sub, model, admission: admission.admissionId, upstream, pool_failed: poolFailure });
|
|
351
|
+
}
|
|
323
352
|
}
|
|
324
|
-
|
|
325
|
-
|
|
353
|
+
if (answer === undefined) {
|
|
354
|
+
try {
|
|
355
|
+
answer = await sendToVendor();
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return refuse(502, 'upstream_unreachable', `${upstream === 'api_key_fallback' ? 'The Claude pool and anthropic' : 'anthropic'} could not be reached.`);
|
|
359
|
+
}
|
|
326
360
|
}
|
|
327
361
|
const responseHeaders = new Headers(answer.headers);
|
|
328
362
|
for (const name of STRIPPED_RESPONSE_HEADERS)
|
|
@@ -365,7 +399,7 @@ export function inferenceEndpoint(options) {
|
|
|
365
399
|
served_model: servedModel,
|
|
366
400
|
units,
|
|
367
401
|
list_usd: list,
|
|
368
|
-
cost_usd:
|
|
402
|
+
cost_usd: upstream === 'pool' ? 0 : list,
|
|
369
403
|
vendor_ref: vendorRef,
|
|
370
404
|
};
|
|
371
405
|
return { settlement, usage };
|
|
@@ -375,7 +409,7 @@ export function inferenceEndpoint(options) {
|
|
|
375
409
|
return;
|
|
376
410
|
const { settlement, usage } = result;
|
|
377
411
|
await record(settlement);
|
|
378
|
-
log({ sub, model, served_model: settlement.served_model, admission: admission.admissionId, upstream
|
|
412
|
+
log({ sub, model, served_model: settlement.served_model, admission: admission.admissionId, upstream, list_usd: settlement.list_usd, cost_usd: settlement.cost_usd, usage });
|
|
379
413
|
}).catch((error) => { log({ sub, admission: admission.admissionId, settle_failed: String(error) }); });
|
|
380
414
|
const charge = calculated.then((result) => result === undefined ? undefined : { id: admission.chargeId, served_model: result.settlement.served_model, list_usd: result.settlement.list_usd }, () => undefined);
|
|
381
415
|
return new Response(chargedBody(toCaller, contentType, charge), { status: answer.status, statusText: answer.statusText, headers: responseHeaders });
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: northlight
|
|
3
3
|
description: "Use when building, debugging or shipping an app that signs people in with Northlight, starts runs on their Northlight runtime, or calls Northlight providers (sms, search, integrations, context, device) with @north-light/sdk."
|
|
4
4
|
metadata:
|
|
5
|
-
sdk-version: "0.3.
|
|
5
|
+
sdk-version: "0.3.633"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Building a Northlight app
|
|
@@ -27,7 +27,7 @@ Never text a person first: Comms refuses a send to anyone who has not texted you
|
|
|
27
27
|
|
|
28
28
|
## First: check this skill matches the SDK
|
|
29
29
|
|
|
30
|
-
This skill is for SDK version `0.3.
|
|
30
|
+
This skill is for SDK version `0.3.633`. If the project has `node_modules/@north-light/sdk/package.json`, compare its `version` with that number. If they differ, run `npx @north-light/sdk skill` in the project, then read the rewritten `.agents/skills/northlight/SKILL.md` (or `.claude/skills/northlight/SKILL.md`) instead of this copy. It needs no account. A project that has no SDK yet starts with `npx @north-light/sdk@latest init <dir>`.
|
|
31
31
|
|
|
32
32
|
## Where things run
|
|
33
33
|
|
package/dist/version.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
|
|
2
|
-
export declare const SDK_VERSION = "0.3.
|
|
2
|
+
export declare const SDK_VERSION = "0.3.633";
|
|
3
3
|
/** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
|
|
4
4
|
* contract level existed (which only knows this header) refuses this SDK, while every later runtime accepts it. */
|
|
5
5
|
export declare const LEGACY_FLOOR = "0.3.423";
|
package/dist/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SDK_VERSION is written by scripts/release-version.mjs on every release; do not edit by hand.
|
|
2
2
|
/** This SDK's release version. Informational only: compatibility is the contract level, not this number. */
|
|
3
|
-
export const SDK_VERSION = '0.3.
|
|
3
|
+
export const SDK_VERSION = '0.3.633';
|
|
4
4
|
// LEGACY_FLOOR is written once by `scripts/contract.mjs release`, at the first release that carries the contract level
|
|
5
5
|
// (the `0.0.0` placeholder is replaced there and never again); do not edit by hand.
|
|
6
6
|
/** The release sent in `Crouter-Minimum-Runtime-Version` on every request. It is frozen, so a runtime from before the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.633",
|
|
4
4
|
"crouterContract": "1.51",
|
|
5
5
|
"description": "Typed Node and browser client for running crouter agents through the crtrd /v1 API.",
|
|
6
6
|
"type": "module",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"prepublishOnly": "node -e \"if (process.env.npm_config_tag !== 'next') { console.error('@north-light/sdk publishes only with --tag next; scripts/promote-sdk.mjs is the only mover of latest.'); process.exit(1); }\""
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@north-light/api": "0.3.
|
|
48
|
-
"@north-light/identity": "0.3.
|
|
49
|
-
"@north-light/plugin": "0.3.
|
|
47
|
+
"@north-light/api": "0.3.633",
|
|
48
|
+
"@north-light/identity": "0.3.633",
|
|
49
|
+
"@north-light/plugin": "0.3.633",
|
|
50
50
|
"jose": "^6.2.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|