@north-light/api 0.3.541 → 0.3.542
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/dist/api/contract.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
|
|
2
|
-
export declare const CONTRACT_LEVEL = "1.
|
|
2
|
+
export declare const CONTRACT_LEVEL = "1.29";
|
|
3
3
|
/** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
|
|
4
4
|
export declare const REQUIRED_CONTRACT_HEADER = "Crouter-Required-Contract";
|
|
5
5
|
/** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
|
package/dist/api/contract.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// PURITY (spec §3.1): Node built-ins + `src/api/*` only.
|
|
14
14
|
/** The contract level this build serves and requires. Written by `scripts/contract.mjs release`; never edit by hand. */
|
|
15
|
-
export const CONTRACT_LEVEL = '1.
|
|
15
|
+
export const CONTRACT_LEVEL = '1.29';
|
|
16
16
|
/** Request header naming the one contract level the caller needs, e.g. `1.3`. Optional: absent means no check. */
|
|
17
17
|
export const REQUIRED_CONTRACT_HEADER = 'Crouter-Required-Contract';
|
|
18
18
|
/** Response header listing the contract levels the server serves, comma-separated, one per major, e.g. `1.3`. */
|
|
@@ -77,18 +77,24 @@ export declare class DuplicateSubscriptionError extends Error {
|
|
|
77
77
|
constructor(message: string);
|
|
78
78
|
}
|
|
79
79
|
export declare function readSubscriptionPool(provider: ManagedProviderId): SubscriptionCredential[];
|
|
80
|
+
/** The pool with each account's row id: the stable handle a caller outside the process removes an account by. */
|
|
81
|
+
export declare function readSubscriptionPoolRows(provider: ManagedProviderId): Array<{
|
|
82
|
+
id: string;
|
|
83
|
+
entry: SubscriptionCredential;
|
|
84
|
+
}>;
|
|
80
85
|
export declare function writeSubscriptionPool(provider: ManagedProviderId, next: SubscriptionCredential[]): Promise<SubscriptionCredential[]>;
|
|
81
86
|
export declare function addSubscription(provider: ManagedProviderId, entry: SubscriptionCredential): Promise<SubscriptionCredential[]>;
|
|
82
87
|
export declare function upsertSubscription(provider: ManagedProviderId, entry: SubscriptionCredential): Promise<SubscriptionCredential[]>;
|
|
83
88
|
export declare function upsertSubscriptionWithUniqueAccount(provider: ManagedProviderId, entry: SubscriptionCredential): Promise<SubscriptionCredential[]>;
|
|
84
89
|
export declare function renameSubscription(provider: ManagedProviderId, ref: string, nextLabel: string): Promise<SubscriptionCredential[]>;
|
|
85
90
|
export declare function removeManagedAccount(provider: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
|
|
91
|
+
/** Remove the account whose row id is `id` (see `readSubscriptionPoolRows`); throws when no account has it. */
|
|
92
|
+
export declare function removeManagedAccountById(provider: ManagedProviderId, id: string): Promise<SubscriptionCredential[]>;
|
|
86
93
|
export declare function removeSubscription(provider: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
|
|
87
94
|
export declare function removeAllManagedAccounts(provider: ManagedProviderId): Promise<number>;
|
|
88
95
|
export declare function removeManagedProvider(provider: ManagedProviderId, label?: string): Promise<SubscriptionCredential[]>;
|
|
89
96
|
export declare function promoteSubscription(provider: ManagedProviderId, ref: string): Promise<SubscriptionCredential[]>;
|
|
90
97
|
export declare function markSubscriptionAttempt(provider: ManagedProviderId, ref: string, attemptAt?: number): Promise<SubscriptionCredential[]>;
|
|
91
|
-
export declare function markSubscriptionAuthenticationFailed(provider: ManagedProviderId, ref: string, attempted: Pick<SubscriptionCredential, 'refresh' | 'access'>, retryAfterMs: number, attemptAt?: number): Promise<SubscriptionCredential[]>;
|
|
92
98
|
/** Record a 429 on an account. `scope` defaults to the whole account; a model scope cools only that family on this account, and a 429 older than the newest one already recorded for the same scope is ignored. */
|
|
93
99
|
export declare function markSubscriptionRateLimited(provider: ManagedProviderId, ref: string, retryAfterMs: number, attemptAt?: number, rateLimitedAt?: number, scope?: RateLimitScope): Promise<SubscriptionCredential[]>;
|
|
94
100
|
/** A call on `modelId` succeeded: clear the account-wide cooldown and auth failure, and that model family's cooldown, unless a newer 429 for the same scope landed after `attemptAt` began. */
|
|
@@ -87,6 +87,8 @@ export class DuplicateSubscriptionError extends Error {
|
|
|
87
87
|
constructor(message) { super(message); this.name = 'DuplicateSubscriptionError'; }
|
|
88
88
|
}
|
|
89
89
|
export function readSubscriptionPool(provider) { return rowsWithCredentials(provider, readFile(provider)).map(({ entry }) => entry); }
|
|
90
|
+
/** The pool with each account's row id: the stable handle a caller outside the process removes an account by. */
|
|
91
|
+
export function readSubscriptionPoolRows(provider) { return rowsWithCredentials(provider, readFile(provider)).map(({ row, entry }) => ({ id: row.id, entry })); }
|
|
90
92
|
async function locked(provider, run) { const path = credentialFile(provider); return managedCredentialFile(provider).withLockAsync(async (current, _held, publish) => { const file = parseCredentials(path, current); return { result: await run(file, rowsWithCredentials(provider, file), (next) => publish(`${JSON.stringify(next)}\n`)) }; }); }
|
|
91
93
|
function insert(db, provider, row) { cachedStatement(db, 'INSERT INTO managed_accounts(provider_id,id,label,label_key,position,last_attempt_at,last_rate_limited_at,rate_limited_until,auth_failure) VALUES(?,?,?,?,?,?,?,?,?)').run(provider, row.id, row.label, row.labelKey, row.position, row.lastAttemptAt, row.lastRateLimitedAt, row.rateLimitedUntil, row.authFailure ?? null); }
|
|
92
94
|
function update(db, row) { cachedStatement(db, 'UPDATE managed_accounts SET label=?,label_key=?,position=?,last_attempt_at=?,last_rate_limited_at=?,rate_limited_until=?,auth_failure=? WHERE provider_id=? AND id=?').run(row.label, row.labelKey, row.position, row.lastAttemptAt, row.lastRateLimitedAt, row.rateLimitedUntil, row.authFailure ?? null, row.providerId, row.id); }
|
|
@@ -130,7 +132,7 @@ export async function upsertSubscriptionWithUniqueAccount(provider, entry) {
|
|
|
130
132
|
const cooldownIncoming = checked.lastRateLimitedAt >= row.lastRateLimitedAt;
|
|
131
133
|
file.accounts[row.id] = stored(checked);
|
|
132
134
|
publish(file);
|
|
133
|
-
writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => { update(db, { ...row, label: checked.label, labelKey: key(checked.label), lastAttemptAt: Math.max(row.lastAttemptAt, checked.lastAttemptAt), lastRateLimitedAt: cooldownIncoming ? checked.lastRateLimitedAt : row.lastRateLimitedAt, rateLimitedUntil: cooldownIncoming ? checked.rateLimitedUntil : row.rateLimitedUntil }); if (differentAccount(existing.entry, checked))
|
|
135
|
+
writeMembershipAfterCredentialPublish(provider, () => providerWrite(agentDir(), (db) => { update(db, { ...row, authFailure: undefined, label: checked.label, labelKey: key(checked.label), lastAttemptAt: Math.max(row.lastAttemptAt, checked.lastAttemptAt), lastRateLimitedAt: cooldownIncoming ? checked.lastRateLimitedAt : row.lastRateLimitedAt, rateLimitedUntil: cooldownIncoming ? checked.rateLimitedUntil : row.rateLimitedUntil }); if (differentAccount(existing.entry, checked))
|
|
134
136
|
deleteModelCooldowns(db, provider, row.id); }));
|
|
135
137
|
return publicPool(provider, file);
|
|
136
138
|
});
|
|
@@ -139,25 +141,24 @@ export async function renameSubscription(provider, ref, nextLabel) { return lock
|
|
|
139
141
|
throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); const label = nextLabel.trim(); if (!label)
|
|
140
142
|
throw new Error(`${getProviderLabel(provider)} subscription label cannot be empty`); if (rows.some((value, i) => i !== index && key(value.entry.label) === key(label)))
|
|
141
143
|
throw new DuplicateSubscriptionError(`${getProviderLabel(provider)} subscription already exists: ${label}`); providerWrite(agentDir(), (db) => update(db, { ...rows[index].row, label, labelKey: key(label) })); return publicPool(provider, file); }); }
|
|
142
|
-
async function remove(provider,
|
|
143
|
-
throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${
|
|
144
|
-
export async function removeManagedAccount(provider, ref) { return remove(provider, ref); }
|
|
145
|
-
|
|
144
|
+
async function remove(provider, find, missing) { return locked(provider, async (file, rows, publish) => { const index = find(rows); if (index < 0)
|
|
145
|
+
throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${missing}`); const removed = rows[index].row; providerWrite(agentDir(), (db) => { cachedStatement(db, 'DELETE FROM managed_accounts WHERE provider_id=? AND id=?').run(provider, removed.id); deleteModelCooldowns(db, provider, removed.id); }); delete file.accounts[removed.id]; publish(file); return publicPool(provider, file); }); }
|
|
146
|
+
export async function removeManagedAccount(provider, ref) { return remove(provider, (rows) => resolve(rows, ref), ref); }
|
|
147
|
+
/** Remove the account whose row id is `id` (see `readSubscriptionPoolRows`); throws when no account has it. */
|
|
148
|
+
export async function removeManagedAccountById(provider, id) { return remove(provider, (rows) => rows.findIndex(({ row }) => row.id === id), id); }
|
|
149
|
+
export async function removeSubscription(provider, ref) { return remove(provider, (rows) => resolve(rows, ref), ref); }
|
|
146
150
|
export async function removeAllManagedAccounts(provider) { return locked(provider, async (file, rows, publish) => { providerWrite(agentDir(), (db) => { cachedStatement(db, 'DELETE FROM managed_accounts WHERE provider_id=?').run(provider); deleteModelCooldowns(db, provider); }); for (const { row } of rows)
|
|
147
151
|
delete file.accounts[row.id]; publish(file); return rows.length; }); }
|
|
148
152
|
export async function removeManagedProvider(provider, label) { if (label === undefined) {
|
|
149
153
|
await removeAllManagedAccounts(provider);
|
|
150
154
|
return [];
|
|
151
|
-
} return
|
|
155
|
+
} return removeManagedAccount(provider, label); }
|
|
152
156
|
export async function promoteSubscription(provider, ref) { return locked(provider, async (file, rows) => { const index = resolve(rows, ref); if (index < 0)
|
|
153
157
|
throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); providerWrite(agentDir(), (db) => { const reordered = [rows[index], ...rows.filter((_, i) => i !== index)]; reordered.forEach((item, position) => update(db, { ...item.row, position })); }); return publicPool(provider, file); }); }
|
|
154
158
|
async function mutateHealth(provider, ref, mutate) { providerWrite(agentDir(), (db) => { const row = providerRowByReference(db, provider, ref); if (!row)
|
|
155
159
|
throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); const next = mutate(row); if (next)
|
|
156
160
|
update(db, next); }); return readSubscriptionPool(provider); }
|
|
157
161
|
export async function markSubscriptionAttempt(provider, ref, attemptAt = Date.now()) { return mutateHealth(provider, ref, (row) => ({ ...row, lastAttemptAt: Math.max(row.lastAttemptAt, attemptAt) })); }
|
|
158
|
-
export async function markSubscriptionAuthenticationFailed(provider, ref, attempted, retryAfterMs, attemptAt = Date.now()) { return locked(provider, async (file, rows) => { const current = rows.find(({ entry }) => key(entry.label) === key(ref)); if (!current)
|
|
159
|
-
throw new Error(`Unknown ${getProviderLabel(provider)} subscription: ${ref}`); if (current.entry.refresh !== attempted.refresh || current.entry.access !== attempted.access)
|
|
160
|
-
return publicPool(provider, file); providerWrite(agentDir(), (db) => update(db, { ...current.row, authFailure: 'invalid_grant', lastAttemptAt: Math.max(current.row.lastAttemptAt, attemptAt), rateLimitedUntil: Math.max(current.row.rateLimitedUntil, attemptAt + Math.max(0, retryAfterMs)) })); return publicPool(provider, file); }); }
|
|
161
162
|
/** Record a 429 on an account. `scope` defaults to the whole account; a model scope cools only that family on this account, and a 429 older than the newest one already recorded for the same scope is ignored. */
|
|
162
163
|
export async function markSubscriptionRateLimited(provider, ref, retryAfterMs, attemptAt = Date.now(), rateLimitedAt = Date.now(), scope = { kind: 'account' }) {
|
|
163
164
|
retryAfterMs = Math.min(Math.max(0, retryAfterMs), MAX_RATE_LIMIT_COOLDOWN_MS);
|
|
@@ -192,9 +193,18 @@ export async function markSubscriptionSuccess(provider, ref, attemptAt = Date.no
|
|
|
192
193
|
});
|
|
193
194
|
return readSubscriptionPool(provider);
|
|
194
195
|
}
|
|
196
|
+
/** The vendor refused this account's refresh token: flag the row so every pool reader skips it until a sign-in replaces it. Both refresh paths (pi's `modify` and the daemon's /v1/llm) record it here. */
|
|
197
|
+
function recordInvalidGrant(row) { const at = Date.now(); providerWrite(agentDir(), (db) => update(db, { ...row, authFailure: 'invalid_grant', lastAttemptAt: Math.max(row.lastAttemptAt, at), rateLimitedUntil: Math.max(row.rateLimitedUntil, at + INVALID_REFRESH_TOKEN_BACKOFF_MS) })); }
|
|
195
198
|
export async function refreshSubscriptionCredential(provider, label, exchange) { return locked(provider, async (file, rows, publish) => { const index = resolve(rows, label); if (index < 0)
|
|
196
199
|
throw new Error(`${getProviderLabel(provider)} subscription "${label}" no longer exists`); const current = rows[index]; if (current.entry.expires > Date.now())
|
|
197
|
-
return current.entry;
|
|
200
|
+
return current.entry; let next; try {
|
|
201
|
+
next = await exchange(current.entry.refresh, current.entry.clientId);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (isInvalidRefreshTokenError(error))
|
|
205
|
+
recordInvalidGrant(current.row);
|
|
206
|
+
throw error;
|
|
207
|
+
} const tokens = withClientId(assertTokens(provider, current.entry.label, next), next.clientId ?? current.entry.clientId); file.accounts[current.row.id] = { ...file.accounts[current.row.id], ...tokens }; publish(file); return { ...current.entry, ...tokens }; }); }
|
|
198
208
|
export function isInvalidRefreshTokenError(error) { const value = error; const message = [value?.errorMessage, value?.message, error instanceof Error ? error.message : ''].filter((part) => typeof part === 'string').join(' '); const status = typeof value?.status === 'number' ? value.status : Number(message.match(/\bstatus[=:]\s*(\d+)/i)?.[1]); return status === 400 && /invalid_grant|refresh token .*invalid|refresh token .*not found/i.test(message); }
|
|
199
209
|
export async function modifyElectedSubscription(provider, elect, apply) { return locked(provider, async (file, rows, publish) => { const attempted = new Set(); let lastError; while (attempted.size < rows.length) {
|
|
200
210
|
const chosen = elect(rows.filter(({ entry }) => !attempted.has(entry.label)).map(({ entry }) => entry));
|
|
@@ -216,8 +226,7 @@ export async function modifyElectedSubscription(provider, elect, apply) { return
|
|
|
216
226
|
if (!isInvalidRefreshTokenError(error))
|
|
217
227
|
throw error;
|
|
218
228
|
lastError = error;
|
|
219
|
-
|
|
220
|
-
providerWrite(agentDir(), (db) => update(db, { ...current.row, authFailure: 'invalid_grant', lastAttemptAt: Math.max(current.row.lastAttemptAt, at), rateLimitedUntil: Math.max(current.row.rateLimitedUntil, at + INVALID_REFRESH_TOKEN_BACKOFF_MS) }));
|
|
229
|
+
recordInvalidGrant(current.row);
|
|
221
230
|
}
|
|
222
231
|
} if (lastError)
|
|
223
232
|
throw lastError; return undefined; }); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/api",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.542",
|
|
4
4
|
"description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/api/index.js",
|