@noodleseed/one 0.148.0 → 0.149.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/dist/commands/assistant-ops.d.ts.map +1 -1
- package/dist/commands/assistant-ops.js +34 -0
- package/dist/commands/assistant-ops.js.map +1 -1
- package/dist/commands/assistant-surface-ops.d.ts +14 -1
- package/dist/commands/assistant-surface-ops.d.ts.map +1 -1
- package/dist/commands/assistant-surface-ops.js +33 -2
- package/dist/commands/assistant-surface-ops.js.map +1 -1
- package/dist/dev-local-runtime.d.ts +1 -0
- package/dist/dev-local-runtime.d.ts.map +1 -1
- package/dist/dev-local-runtime.js +3 -0
- package/dist/dev-local-runtime.js.map +1 -1
- package/node_modules/@noodle-borg/admission-limits/dist/counter-store.d.ts +20 -0
- package/node_modules/@noodle-borg/admission-limits/dist/counter-store.js +12 -0
- package/node_modules/@noodle-borg/admission-limits/dist/envelope.d.ts +34 -2
- package/node_modules/@noodle-borg/admission-limits/dist/envelope.js +35 -11
- package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.d.ts +1 -0
- package/node_modules/@noodle-borg/admission-limits/dist/in-memory-counter-store.js +14 -1
- package/node_modules/@noodle-borg/admission-limits/dist/portable.d.ts +1 -0
- package/node_modules/@noodle-borg/admission-limits/dist/portable.js +1 -0
- package/node_modules/@noodle-borg/admission-limits/dist/visitor-bucket.d.ts +20 -0
- package/node_modules/@noodle-borg/admission-limits/dist/visitor-bucket.js +30 -0
- package/node_modules/@noodle-borg/agent-kit/dist/skill-tool-design-ref.js +4 -0
- package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.d.ts +12 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.js +15 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.d.ts +71 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-operator-view.js +59 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +27 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.js +35 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +6 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.d.ts +80 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/managed-spend.js +186 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.d.ts +16 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.d.ts +14 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.js +23 -6
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.js +22 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +15 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +14 -0
- package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +15 -1
- package/node_modules/@noodle-borg/module/dist/request-analytics.d.ts +19 -2
- package/node_modules/@noodle-borg/module/dist/request-analytics.js +15 -2
- package/node_modules/@noodle-borg/observability/dist/assistant-usage.js +68 -10
- package/node_modules/@noodle-borg/observability/dist/index.js +1 -0
- package/node_modules/@noodle-borg/observability/dist/request-event-query.js +48 -0
- package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +13 -2
- package/node_modules/@noodle-borg/observability/dist/request-events.js +3 -0
- package/node_modules/@noodle-borg/service/dist/routes/analytics.js +5 -29
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +57 -29
- package/node_modules/@noodle-borg/service/dist/routes/assistant-embeds.js +26 -38
- package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +1 -4
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +7 -3
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +14 -8
- package/node_modules/@noodle-borg/service/package.json +1 -1
- package/node_modules/@noodle-borg/transport-http/dist/request-capture.js +2 -0
- package/node_modules/@noodleseed/assistant/package.json +1 -1
- package/package.json +2 -2
|
@@ -31,6 +31,21 @@ export function selectAssistantModelTools(artifact, caller, options = {}) {
|
|
|
31
31
|
hasValidSessionVisibility(tool, options.usedToolNames) &&
|
|
32
32
|
matchesLatestMessageConstraint(tool, options.latestMessage));
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* What to tell a model that called a tool this turn does not offer.
|
|
36
|
+
*
|
|
37
|
+
* The alternative — ending the turn on `invalid_model_tool_call` — is a security-correct rejection
|
|
38
|
+
* with a dishonest failure mode: the widget renders it as "temporarily unavailable" beside a
|
|
39
|
+
* Reconnect button that mints a whole new session, so a visitor who asked for something reasonable
|
|
40
|
+
* gets silence and the surface pays a session for it. A `role:'tool'` result costs one model step
|
|
41
|
+
* and gets the visitor a real answer instead. It names the tool because the model supplied that
|
|
42
|
+
* name in the first place, and says nothing about why the tool is absent, which is the part a
|
|
43
|
+
* visitor is not entitled to.
|
|
44
|
+
*/
|
|
45
|
+
export function assistantOmittedToolResult(name) {
|
|
46
|
+
return (`The tool "${name}" is not available in this conversation. Do not call it again. ` +
|
|
47
|
+
'Answer the person directly in your own words, and say plainly if you cannot help with this.');
|
|
48
|
+
}
|
|
34
49
|
/** Whether an already-selected tool requires the first model step to call it. */
|
|
35
50
|
export function assistantModelToolRequiredWhenVisible(tool) {
|
|
36
51
|
return tool.annotations?.[REQUIRED_WHEN_VISIBLE] === true;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AdmissionEnvelope } from '@noodle-borg/admission-limits/portable';
|
|
2
|
+
import type { PublicEmbedRecord } from './embed-store.js';
|
|
3
|
+
import type { PublicSurface } from './public-surface.js';
|
|
4
|
+
/**
|
|
5
|
+
* The operator's view of one public surface: what it is, and what it may spend.
|
|
6
|
+
*
|
|
7
|
+
* Origins, capabilities, and surfaceMode come from the *active* deployment rather than the embed
|
|
8
|
+
* row, so what an operator reads here is what a browser will actually be held to at mint time. The
|
|
9
|
+
* row keeps its provisioning-time mode by design — `ensure` never overwrites a live row.
|
|
10
|
+
*/
|
|
11
|
+
export interface AssistantEmbedOperatorView {
|
|
12
|
+
readonly embedId: string;
|
|
13
|
+
readonly surfaceMode: string;
|
|
14
|
+
readonly origins: readonly string[];
|
|
15
|
+
readonly capabilities: readonly string[];
|
|
16
|
+
readonly turnsPerDay: number;
|
|
17
|
+
readonly mintsPerDay: number;
|
|
18
|
+
/** True when the caps are the deployment defaults rather than something an operator chose. */
|
|
19
|
+
readonly budgetIsDefault: boolean;
|
|
20
|
+
readonly turnsToday: number;
|
|
21
|
+
readonly mintsToday: number;
|
|
22
|
+
readonly createdAt: string;
|
|
23
|
+
/**
|
|
24
|
+
* Present only when Noodle is funding this surface's model. It exists so an operator can tell
|
|
25
|
+
* platform-imposed degradation from a bug in their own app, which is the one thing they cannot
|
|
26
|
+
* work out from the caps above — those look normal right up to the moment the ladder closes them.
|
|
27
|
+
*/
|
|
28
|
+
readonly managedSpend?: AssistantManagedSpendView;
|
|
29
|
+
}
|
|
30
|
+
export interface AssistantManagedSpendView {
|
|
31
|
+
/** `normal`, `approaching`, `near`, `limited`, or `closed`. */
|
|
32
|
+
readonly state: string;
|
|
33
|
+
/**
|
|
34
|
+
* Turns left today at the current rung, not a percentage. The spend consume is all-or-nothing, so
|
|
35
|
+
* a costly turn is refused while a cheap one still passes at the tail of the day — a percentage
|
|
36
|
+
* would look like it stopped early.
|
|
37
|
+
*/
|
|
38
|
+
readonly turnsRemaining: number;
|
|
39
|
+
/** One plain sentence about what a visitor experiences right now. */
|
|
40
|
+
readonly visitors: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Project a tenant's spend into what an operator needs to read.
|
|
44
|
+
*
|
|
45
|
+
* Takes the raw peek rather than a rung so the caller owns only the read: which rung that is, and
|
|
46
|
+
* what a turn costs there, are the ladder's business and stay in one place.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assistantManagedSpendView(input: {
|
|
49
|
+
readonly spend: {
|
|
50
|
+
readonly allowance: number;
|
|
51
|
+
};
|
|
52
|
+
readonly unitsUsed: number;
|
|
53
|
+
}): AssistantManagedSpendView;
|
|
54
|
+
/**
|
|
55
|
+
* Project one embed row into that view. Pure on purpose: the caller owns the reads (active
|
|
56
|
+
* deployment, counter peeks) and this owns the shape, so the projection stays testable without a
|
|
57
|
+
* registry or a counter store.
|
|
58
|
+
*/
|
|
59
|
+
export declare function assistantEmbedOperatorView(input: {
|
|
60
|
+
readonly record: PublicEmbedRecord;
|
|
61
|
+
readonly surface: PublicSurface | undefined;
|
|
62
|
+
readonly envelope: AdmissionEnvelope;
|
|
63
|
+
/** Usage without consuming. The keys are this package's own convention, so it forms them here. */
|
|
64
|
+
readonly peek: (key: string) => Promise<number>;
|
|
65
|
+
/** Present only when Noodle funds this surface's model. */
|
|
66
|
+
readonly spend?: {
|
|
67
|
+
readonly key: string;
|
|
68
|
+
readonly allowance: number;
|
|
69
|
+
};
|
|
70
|
+
}): Promise<AssistantEmbedOperatorView>;
|
|
71
|
+
//# sourceMappingURL=embed-operator-view.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { managedSpendRung, managedSpendUnitWeight } from './managed-spend.js';
|
|
2
|
+
const SPEND_STATES = [
|
|
3
|
+
{ state: 'normal', visitors: 'Full answers, full tools.' },
|
|
4
|
+
{ state: 'approaching', visitors: 'Shorter answers. Nothing else changes.' },
|
|
5
|
+
{
|
|
6
|
+
state: 'near',
|
|
7
|
+
visitors: 'Shorter answers, and conversations restart after 8 turns.',
|
|
8
|
+
},
|
|
9
|
+
{ state: 'limited', visitors: 'Short answers from knowledge only; tools are paused.' },
|
|
10
|
+
{
|
|
11
|
+
state: 'closed',
|
|
12
|
+
visitors: 'Paused until 00:00 UTC. Connect your own model key to keep serving.',
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Project a tenant's spend into what an operator needs to read.
|
|
17
|
+
*
|
|
18
|
+
* Takes the raw peek rather than a rung so the caller owns only the read: which rung that is, and
|
|
19
|
+
* what a turn costs there, are the ladder's business and stay in one place.
|
|
20
|
+
*/
|
|
21
|
+
export function assistantManagedSpendView(input) {
|
|
22
|
+
const rungAt = managedSpendRung(input.unitsUsed, input.spend.allowance);
|
|
23
|
+
const perTurn = managedSpendUnitWeight(rungAt);
|
|
24
|
+
const remaining = Math.max(0, input.spend.allowance - input.unitsUsed);
|
|
25
|
+
const rung = Math.min(rungAt, SPEND_STATES.length - 1);
|
|
26
|
+
return {
|
|
27
|
+
...SPEND_STATES[rung],
|
|
28
|
+
turnsRemaining: perTurn > 0 ? Math.floor(remaining / perTurn) : 0,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Project one embed row into that view. Pure on purpose: the caller owns the reads (active
|
|
33
|
+
* deployment, counter peeks) and this owns the shape, so the projection stays testable without a
|
|
34
|
+
* registry or a counter store.
|
|
35
|
+
*/
|
|
36
|
+
export async function assistantEmbedOperatorView(input) {
|
|
37
|
+
const { record, surface, envelope, spend } = input;
|
|
38
|
+
return {
|
|
39
|
+
embedId: record.embedId,
|
|
40
|
+
surfaceMode: surface?.mode ?? record.surfaceMode,
|
|
41
|
+
origins: surface?.origins ?? [],
|
|
42
|
+
capabilities: surface?.capabilities.map((entry) => entry.name) ?? [],
|
|
43
|
+
turnsPerDay: envelope.turnsPerDay,
|
|
44
|
+
mintsPerDay: envelope.mintsPerDay,
|
|
45
|
+
budgetIsDefault: record.turnsPerDay === undefined && record.mintsPerDay === undefined,
|
|
46
|
+
turnsToday: await input.peek(`turns:${record.embedId}`),
|
|
47
|
+
mintsToday: await input.peek(`mints:${record.embedId}`),
|
|
48
|
+
createdAt: record.createdAt.toISOString(),
|
|
49
|
+
...(spend === undefined
|
|
50
|
+
? {}
|
|
51
|
+
: {
|
|
52
|
+
managedSpend: assistantManagedSpendView({
|
|
53
|
+
spend,
|
|
54
|
+
unitsUsed: await input.peek(spend.key),
|
|
55
|
+
}),
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=embed-operator-view.js.map
|
|
@@ -31,6 +31,13 @@ export interface PublicEmbedRecord {
|
|
|
31
31
|
*/
|
|
32
32
|
readonly turnsPerDay?: number;
|
|
33
33
|
readonly mintsPerDay?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Operator overrides for the per-address abuse bounds. Rarely needed — tier 4 handles fairness and
|
|
36
|
+
* these only have to stop a single machine — but an operator who knows their own traffic shape
|
|
37
|
+
* (an internal tool behind one proxy, say) can lower them, and lowering is all anyone may do.
|
|
38
|
+
*/
|
|
39
|
+
readonly mintsPerAddressHour?: number;
|
|
40
|
+
readonly turnsPerAddressHour?: number;
|
|
34
41
|
}
|
|
35
42
|
/** Project the non-secret embed coordinate onto the shared assistant tenant shape. */
|
|
36
43
|
export declare function publicEmbedTenant(embed: PublicEmbedRecord): TenantRef;
|
|
@@ -38,6 +45,8 @@ export declare function publicEmbedTenant(embed: PublicEmbedRecord): TenantRef;
|
|
|
38
45
|
export interface PublicEmbedBudget {
|
|
39
46
|
readonly turnsPerDay?: number;
|
|
40
47
|
readonly mintsPerDay?: number;
|
|
48
|
+
readonly mintsPerAddressHour?: number;
|
|
49
|
+
readonly turnsPerAddressHour?: number;
|
|
41
50
|
}
|
|
42
51
|
export interface PublicEmbedStore {
|
|
43
52
|
/**
|
|
@@ -73,4 +82,22 @@ export interface PublicEmbedStore {
|
|
|
73
82
|
export declare const PUBLIC_EMBED_ID_PREFIX = "pub_";
|
|
74
83
|
export declare function isPublicEmbedId(value: string): boolean;
|
|
75
84
|
import type { TenantRef } from './tenant-ref.js';
|
|
85
|
+
/**
|
|
86
|
+
* Validate an operator's requested caps.
|
|
87
|
+
*
|
|
88
|
+
* Lives with the budget type rather than with the route that receives it: the rules are about what a
|
|
89
|
+
* cap may be, not about HTTP, and a second caller must not be able to reach the store with a shape
|
|
90
|
+
* this would have rejected.
|
|
91
|
+
*/
|
|
92
|
+
export type BudgetField = 'turnsPerDay' | 'mintsPerDay' | 'mintsPerAddressHour' | 'turnsPerAddressHour';
|
|
93
|
+
type BudgetValue = Partial<Record<BudgetField, number>>;
|
|
94
|
+
type ParsedBudget = {
|
|
95
|
+
readonly ok: true;
|
|
96
|
+
readonly value: BudgetValue;
|
|
97
|
+
} | {
|
|
98
|
+
readonly ok: false;
|
|
99
|
+
readonly error: string;
|
|
100
|
+
};
|
|
101
|
+
export declare function parsePublicEmbedBudget(body: unknown): ParsedBudget;
|
|
102
|
+
export {};
|
|
76
103
|
//# sourceMappingURL=embed-store.d.ts.map
|
|
@@ -23,4 +23,39 @@ export const PUBLIC_EMBED_ID_PREFIX = 'pub_';
|
|
|
23
23
|
export function isPublicEmbedId(value) {
|
|
24
24
|
return new RegExp(`^${PUBLIC_EMBED_ID_PREFIX}[0-9a-z]{20,32}$`).test(value);
|
|
25
25
|
}
|
|
26
|
+
const BUDGET_FIELDS = [
|
|
27
|
+
'turnsPerDay',
|
|
28
|
+
'mintsPerDay',
|
|
29
|
+
'mintsPerAddressHour',
|
|
30
|
+
'turnsPerAddressHour',
|
|
31
|
+
];
|
|
32
|
+
export function parsePublicEmbedBudget(body) {
|
|
33
|
+
// A JSON body of `null` is an object to `typeof` and reads as a crash to a property access, so it
|
|
34
|
+
// is checked by name: an operator sending nonsense deserves the 400 this returns, not the 500 an
|
|
35
|
+
// uncaught `TypeError` would become.
|
|
36
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
37
|
+
return { ok: false, error: 'body must be a JSON object' };
|
|
38
|
+
}
|
|
39
|
+
const supplied = body;
|
|
40
|
+
const value = {};
|
|
41
|
+
for (const [name, raw] of BUDGET_FIELDS.map((field) => [field, supplied[field]])) {
|
|
42
|
+
if (raw === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
// Zero is valid and is the point; anything negative or fractional is a mistake worth saying out
|
|
45
|
+
// loud rather than silently flooring, because an operator setting a cap is being deliberate.
|
|
46
|
+
// `isSafeInteger` rather than `isInteger`: past 2^53 a JSON number has already lost the value
|
|
47
|
+
// the operator typed, and it reaches a `::bigint` column that would store the rounded one.
|
|
48
|
+
if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 0) {
|
|
49
|
+
return { ok: false, error: `"${name}" must be a non-negative integer` };
|
|
50
|
+
}
|
|
51
|
+
value[name] = raw;
|
|
52
|
+
}
|
|
53
|
+
if (Object.keys(value).length === 0) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: `set at least one of ${BUDGET_FIELDS.map((f) => `"${f}"`).join(', ')}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { ok: true, value };
|
|
60
|
+
}
|
|
26
61
|
//# sourceMappingURL=embed-store.js.map
|
|
@@ -39,6 +39,12 @@ export class InMemoryPublicEmbedStore {
|
|
|
39
39
|
...record,
|
|
40
40
|
...(budget.turnsPerDay !== undefined ? { turnsPerDay: budget.turnsPerDay } : {}),
|
|
41
41
|
...(budget.mintsPerDay !== undefined ? { mintsPerDay: budget.mintsPerDay } : {}),
|
|
42
|
+
...(budget.mintsPerAddressHour !== undefined
|
|
43
|
+
? { mintsPerAddressHour: budget.mintsPerAddressHour }
|
|
44
|
+
: {}),
|
|
45
|
+
...(budget.turnsPerAddressHour !== undefined
|
|
46
|
+
? { turnsPerAddressHour: budget.turnsPerAddressHour }
|
|
47
|
+
: {}),
|
|
42
48
|
};
|
|
43
49
|
this.#byId.set(embedId, updated);
|
|
44
50
|
return updated;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AdmissionEnvelope } from '@noodle-borg/admission-limits/portable';
|
|
2
|
+
import type { AssistantModelRequestPolicy } from './model-request.js';
|
|
3
|
+
import type { SurfaceBudgetBounds } from './surface-budget.js';
|
|
4
|
+
/**
|
|
5
|
+
* Cost breakers for the sponsored managed-assistant beta (ADR 0213 amendment §3).
|
|
6
|
+
*
|
|
7
|
+
* Noodle pays for `noodle-managed` inference and charges nothing for it, so aggregate spend is a
|
|
8
|
+
* platform expense with no customer-side limit standing behind it. Per-surface daily caps bound one
|
|
9
|
+
* surface; they do not bound how many surfaces enrol. This is the missing ceiling, expressed as a
|
|
10
|
+
* ladder rather than a switch: a tenant approaching its allowance gets shorter answers before it gets
|
|
11
|
+
* a closed door, and the door itself is the kill switch that already exists.
|
|
12
|
+
*
|
|
13
|
+
* Two things make it small. Every rung is expressed through `SurfaceBudgetBounds` and
|
|
14
|
+
* `AssistantModelRequestPolicy`, which the resolver already returns and admission already reads live
|
|
15
|
+
* per turn — so degradation applies immediately and reverses on its own when the UTC day rolls, with
|
|
16
|
+
* no unwind path to write. And the unit is one delivered bounded turn, which is the billable unit ADR
|
|
17
|
+
* 0213 §12 already defines.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Billable tokens per charged unit. Prompt and completion are counted together and completion is
|
|
21
|
+
* weighted, because output costs several times input on every provider we have measured.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MANAGED_SPEND_UNIT_TOKENS = 4000;
|
|
24
|
+
export interface ManagedSpendPolicy {
|
|
25
|
+
readonly requestPolicy: AssistantModelRequestPolicy;
|
|
26
|
+
readonly admission: SurfaceBudgetBounds;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Which rung a tenant is on.
|
|
30
|
+
*
|
|
31
|
+
* An absent or nonsensical allowance means "unbudgeted", never "exhausted". Failing closed on a typo
|
|
32
|
+
* would take a customer's surface dark for a configuration mistake, and the per-surface daily cap
|
|
33
|
+
* still bounds what that costs.
|
|
34
|
+
*/
|
|
35
|
+
export declare function managedSpendRung(unitsUsed: number, allowance: number): number;
|
|
36
|
+
export declare function managedSpendPolicy(rung: number): ManagedSpendPolicy;
|
|
37
|
+
/**
|
|
38
|
+
* Units one turn costs at this rung: its worst case under the rung's own policy, in whole units.
|
|
39
|
+
*
|
|
40
|
+
* Prompt-inclusive on purpose. `maxCompletionTokens` says nothing about how large a prompt the agent
|
|
41
|
+
* loop may resend on each step, and the prompt is where an unbounded turn actually spends.
|
|
42
|
+
*/
|
|
43
|
+
export declare function managedSpendUnitWeight(rung: number): number;
|
|
44
|
+
/**
|
|
45
|
+
* The unit ledger for a day's worth of ordinary turns.
|
|
46
|
+
*
|
|
47
|
+
* Units are worst-case, so a turn is charged what its rung's policy *permits* rather than what it
|
|
48
|
+
* happened to use. That is deliberate — the alternative under-charges exactly the adversary the cap
|
|
49
|
+
* exists for — but it means the number is only meaningful denominated in turns. An allowance is
|
|
50
|
+
* therefore configured as "this many sponsored turns a day" and converted here, which is also the
|
|
51
|
+
* form the operator surface reports.
|
|
52
|
+
*
|
|
53
|
+
* The conservatism is uniform within a rung, so it does not distort when the ladder trips; across
|
|
54
|
+
* rungs it is the intended effect, since a degraded turn genuinely costs less and the same allowance
|
|
55
|
+
* stretches further.
|
|
56
|
+
*/
|
|
57
|
+
export declare function managedSpendAllowanceForTurns(turnsPerDay: number): number;
|
|
58
|
+
/**
|
|
59
|
+
* Is the ledger telling the truth?
|
|
60
|
+
*
|
|
61
|
+
* A unit weight is a claim about what a turn's own rung permits. If the rung's `requestPolicy` does
|
|
62
|
+
* not actually bound what it says, the claim is fiction and the whole cap is decorative — and the
|
|
63
|
+
* failure is silent, because a cheap-looking ledger is exactly what an unbounded turn produces.
|
|
64
|
+
*
|
|
65
|
+
* So telemetry checks the counter rather than replacing it. Observed billable tokens per delivered
|
|
66
|
+
* turn must stay under what rung 0 permits, since rung 0 is the most permissive rung any turn could
|
|
67
|
+
* have run at. Exceeding it is not a budgeting question; it means an enforcer is not enforcing.
|
|
68
|
+
*/
|
|
69
|
+
export declare function managedSpendDrift(observed: {
|
|
70
|
+
readonly deliveredTurns: number;
|
|
71
|
+
readonly promptTokens: number;
|
|
72
|
+
readonly completionTokens: number;
|
|
73
|
+
}): {
|
|
74
|
+
readonly observedPerTurn: number;
|
|
75
|
+
readonly permittedPerTurn: number;
|
|
76
|
+
readonly exceeded: boolean;
|
|
77
|
+
};
|
|
78
|
+
/** Convenience for readers that only care whether a tenant is being held back at all. */
|
|
79
|
+
export declare function isManagedSpendDegraded(envelope: Pick<AdmissionEnvelope, 'turnsPerDay'>): boolean;
|
|
80
|
+
//# sourceMappingURL=managed-spend.d.ts.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost breakers for the sponsored managed-assistant beta (ADR 0213 amendment §3).
|
|
3
|
+
*
|
|
4
|
+
* Noodle pays for `noodle-managed` inference and charges nothing for it, so aggregate spend is a
|
|
5
|
+
* platform expense with no customer-side limit standing behind it. Per-surface daily caps bound one
|
|
6
|
+
* surface; they do not bound how many surfaces enrol. This is the missing ceiling, expressed as a
|
|
7
|
+
* ladder rather than a switch: a tenant approaching its allowance gets shorter answers before it gets
|
|
8
|
+
* a closed door, and the door itself is the kill switch that already exists.
|
|
9
|
+
*
|
|
10
|
+
* Two things make it small. Every rung is expressed through `SurfaceBudgetBounds` and
|
|
11
|
+
* `AssistantModelRequestPolicy`, which the resolver already returns and admission already reads live
|
|
12
|
+
* per turn — so degradation applies immediately and reverses on its own when the UTC day rolls, with
|
|
13
|
+
* no unwind path to write. And the unit is one delivered bounded turn, which is the billable unit ADR
|
|
14
|
+
* 0213 §12 already defines.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Billable tokens per charged unit. Prompt and completion are counted together and completion is
|
|
18
|
+
* weighted, because output costs several times input on every provider we have measured.
|
|
19
|
+
*/
|
|
20
|
+
export const MANAGED_SPEND_UNIT_TOKENS = 4_000;
|
|
21
|
+
const OUTPUT_WEIGHT = 8;
|
|
22
|
+
const BYTES_PER_TOKEN = 4;
|
|
23
|
+
/**
|
|
24
|
+
* The rungs, tightest-claim-first.
|
|
25
|
+
*
|
|
26
|
+
* Rung 0 is not "today's policy": a weight is only honest if the rung's own policy enforces it, and
|
|
27
|
+
* an unbounded prompt makes the worst case forty times the typical turn. Bounding the request keeps
|
|
28
|
+
* the ledger true. These numbers are the starting point and are expected to move once measured P99
|
|
29
|
+
* prompt sizes are read off the live surface — that measurement is a gate on raising the ceilings,
|
|
30
|
+
* not on shipping the breaker.
|
|
31
|
+
*/
|
|
32
|
+
const RUNGS = [
|
|
33
|
+
{
|
|
34
|
+
requestPolicy: {
|
|
35
|
+
maxRequestBytes: 32 * 1_024,
|
|
36
|
+
maxCompletionTokens: 1_500,
|
|
37
|
+
maxTokensPerTurn: 3_000,
|
|
38
|
+
maxModelStepsPerTurn: 6,
|
|
39
|
+
maxToolCallsPerTurn: 8,
|
|
40
|
+
},
|
|
41
|
+
admission: {
|
|
42
|
+
defaults: { turnsPerSession: 40, turnsPerDay: 20_000, mintsPerDay: 5_000 },
|
|
43
|
+
ceiling: { turnsPerDay: 20_000, mintsPerDay: 5_000 },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
// Shorter answers. Nothing is announced: telling a stranger on a customer's marketing site that
|
|
48
|
+
// it is on a reduced tier harms the customer and helps no one, and a shorter answer explains
|
|
49
|
+
// itself.
|
|
50
|
+
requestPolicy: {
|
|
51
|
+
maxRequestBytes: 24 * 1_024,
|
|
52
|
+
maxCompletionTokens: 900,
|
|
53
|
+
maxTokensPerTurn: 1_800,
|
|
54
|
+
maxModelStepsPerTurn: 4,
|
|
55
|
+
maxToolCallsPerTurn: 6,
|
|
56
|
+
},
|
|
57
|
+
admission: {
|
|
58
|
+
defaults: { turnsPerSession: 40, turnsPerDay: 20_000, mintsPerDay: 5_000 },
|
|
59
|
+
ceiling: { turnsPerDay: 20_000, mintsPerDay: 5_000 },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
// Shorter conversations. This does not lower a turn's worst case — `maxRequestBytes` already caps
|
|
64
|
+
// that — it lowers the typical prompt by cutting history growth, which stretches the remaining
|
|
65
|
+
// allowance without shortening any individual answer. The visitor meets
|
|
66
|
+
// `session_turn_budget_exhausted`, which the widget already renders as its existing "new
|
|
67
|
+
// conversation" affordance.
|
|
68
|
+
requestPolicy: {
|
|
69
|
+
maxRequestBytes: 16 * 1_024,
|
|
70
|
+
maxCompletionTokens: 700,
|
|
71
|
+
maxTokensPerTurn: 1_400,
|
|
72
|
+
maxModelStepsPerTurn: 3,
|
|
73
|
+
maxToolCallsPerTurn: 4,
|
|
74
|
+
},
|
|
75
|
+
admission: {
|
|
76
|
+
defaults: { turnsPerSession: 8, turnsPerDay: 20_000, mintsPerDay: 5_000 },
|
|
77
|
+
ceiling: { turnsPerDay: 20_000, mintsPerDay: 5_000 },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
// No tools. `projectAssistantGuide` projects the per-turn tool set, so the guide never promises a
|
|
82
|
+
// tool that is not offered; the assistant simply answers from knowledge and its own words.
|
|
83
|
+
requestPolicy: {
|
|
84
|
+
maxRequestBytes: 12 * 1_024,
|
|
85
|
+
maxCompletionTokens: 400,
|
|
86
|
+
maxTokensPerTurn: 800,
|
|
87
|
+
maxModelStepsPerTurn: 1,
|
|
88
|
+
maxToolCallsPerTurn: 0,
|
|
89
|
+
},
|
|
90
|
+
admission: {
|
|
91
|
+
defaults: { turnsPerSession: 8, turnsPerDay: 20_000, mintsPerDay: 5_000 },
|
|
92
|
+
ceiling: { turnsPerDay: 20_000, mintsPerDay: 5_000 },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
// At cap. Deliberately the *existing* kill switch and not a new failure mode: zero ceilings refuse
|
|
97
|
+
// at admission with `daily_turn_budget_exhausted`, a code every published widget already treats as
|
|
98
|
+
// final. A new code would be read as retryable by every widget predating it, and they would hammer
|
|
99
|
+
// the endpoint this cap exists to protect.
|
|
100
|
+
requestPolicy: {
|
|
101
|
+
maxRequestBytes: 12 * 1_024,
|
|
102
|
+
maxCompletionTokens: 400,
|
|
103
|
+
maxTokensPerTurn: 800,
|
|
104
|
+
maxModelStepsPerTurn: 1,
|
|
105
|
+
maxToolCallsPerTurn: 0,
|
|
106
|
+
},
|
|
107
|
+
admission: {
|
|
108
|
+
defaults: { turnsPerSession: 8, turnsPerDay: 0, mintsPerDay: 0 },
|
|
109
|
+
ceiling: { turnsPerDay: 0, mintsPerDay: 0 },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
/** Fractions of the allowance at which each rung begins. */
|
|
114
|
+
const THRESHOLDS = [0.6, 0.8, 0.95, 1];
|
|
115
|
+
/**
|
|
116
|
+
* Which rung a tenant is on.
|
|
117
|
+
*
|
|
118
|
+
* An absent or nonsensical allowance means "unbudgeted", never "exhausted". Failing closed on a typo
|
|
119
|
+
* would take a customer's surface dark for a configuration mistake, and the per-surface daily cap
|
|
120
|
+
* still bounds what that costs.
|
|
121
|
+
*/
|
|
122
|
+
export function managedSpendRung(unitsUsed, allowance) {
|
|
123
|
+
if (!Number.isFinite(allowance) || allowance <= 0)
|
|
124
|
+
return 0;
|
|
125
|
+
const share = Math.max(0, unitsUsed) / allowance;
|
|
126
|
+
return THRESHOLDS.filter((threshold) => share >= threshold).length;
|
|
127
|
+
}
|
|
128
|
+
export function managedSpendPolicy(rung) {
|
|
129
|
+
return RUNGS[Math.min(Math.max(Math.trunc(rung), 0), RUNGS.length - 1)];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Units one turn costs at this rung: its worst case under the rung's own policy, in whole units.
|
|
133
|
+
*
|
|
134
|
+
* Prompt-inclusive on purpose. `maxCompletionTokens` says nothing about how large a prompt the agent
|
|
135
|
+
* loop may resend on each step, and the prompt is where an unbounded turn actually spends.
|
|
136
|
+
*/
|
|
137
|
+
export function managedSpendUnitWeight(rung) {
|
|
138
|
+
const { requestPolicy } = managedSpendPolicy(rung);
|
|
139
|
+
const prompt = ((requestPolicy.maxModelStepsPerTurn ?? 1) * (requestPolicy.maxRequestBytes ?? 0)) /
|
|
140
|
+
BYTES_PER_TOKEN;
|
|
141
|
+
const output = OUTPUT_WEIGHT * (requestPolicy.maxTokensPerTurn ?? 0);
|
|
142
|
+
return Math.max(1, Math.ceil((prompt + output) / MANAGED_SPEND_UNIT_TOKENS));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The unit ledger for a day's worth of ordinary turns.
|
|
146
|
+
*
|
|
147
|
+
* Units are worst-case, so a turn is charged what its rung's policy *permits* rather than what it
|
|
148
|
+
* happened to use. That is deliberate — the alternative under-charges exactly the adversary the cap
|
|
149
|
+
* exists for — but it means the number is only meaningful denominated in turns. An allowance is
|
|
150
|
+
* therefore configured as "this many sponsored turns a day" and converted here, which is also the
|
|
151
|
+
* form the operator surface reports.
|
|
152
|
+
*
|
|
153
|
+
* The conservatism is uniform within a rung, so it does not distort when the ladder trips; across
|
|
154
|
+
* rungs it is the intended effect, since a degraded turn genuinely costs less and the same allowance
|
|
155
|
+
* stretches further.
|
|
156
|
+
*/
|
|
157
|
+
export function managedSpendAllowanceForTurns(turnsPerDay) {
|
|
158
|
+
if (!Number.isFinite(turnsPerDay) || turnsPerDay <= 0)
|
|
159
|
+
return 0;
|
|
160
|
+
return Math.ceil(turnsPerDay) * managedSpendUnitWeight(0);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Is the ledger telling the truth?
|
|
164
|
+
*
|
|
165
|
+
* A unit weight is a claim about what a turn's own rung permits. If the rung's `requestPolicy` does
|
|
166
|
+
* not actually bound what it says, the claim is fiction and the whole cap is decorative — and the
|
|
167
|
+
* failure is silent, because a cheap-looking ledger is exactly what an unbounded turn produces.
|
|
168
|
+
*
|
|
169
|
+
* So telemetry checks the counter rather than replacing it. Observed billable tokens per delivered
|
|
170
|
+
* turn must stay under what rung 0 permits, since rung 0 is the most permissive rung any turn could
|
|
171
|
+
* have run at. Exceeding it is not a budgeting question; it means an enforcer is not enforcing.
|
|
172
|
+
*/
|
|
173
|
+
export function managedSpendDrift(observed) {
|
|
174
|
+
const permittedPerTurn = managedSpendUnitWeight(0) * MANAGED_SPEND_UNIT_TOKENS;
|
|
175
|
+
if (observed.deliveredTurns <= 0) {
|
|
176
|
+
return { observedPerTurn: 0, permittedPerTurn, exceeded: false };
|
|
177
|
+
}
|
|
178
|
+
const billable = observed.promptTokens + OUTPUT_WEIGHT * observed.completionTokens;
|
|
179
|
+
const observedPerTurn = Math.round(billable / observed.deliveredTurns);
|
|
180
|
+
return { observedPerTurn, permittedPerTurn, exceeded: observedPerTurn > permittedPerTurn };
|
|
181
|
+
}
|
|
182
|
+
/** Convenience for readers that only care whether a tenant is being held back at all. */
|
|
183
|
+
export function isManagedSpendDegraded(envelope) {
|
|
184
|
+
return envelope.turnsPerDay === 0;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=managed-spend.js.map
|
|
@@ -5,6 +5,12 @@ export interface AssistantModelRequestPolicy {
|
|
|
5
5
|
readonly maxCompletionTokens?: number;
|
|
6
6
|
readonly maxTokensPerTurn?: number;
|
|
7
7
|
readonly maxRequestBytes?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Per-tenant agent-loop bounds. The deployment-wide admission envelope still wins whenever it is
|
|
10
|
+
* lower — these narrow a turn for one tenant, they never widen it for anyone.
|
|
11
|
+
*/
|
|
12
|
+
readonly maxModelStepsPerTurn?: number;
|
|
13
|
+
readonly maxToolCallsPerTurn?: number;
|
|
8
14
|
readonly timeoutMs?: number;
|
|
9
15
|
readonly maxTurnMs?: number;
|
|
10
16
|
/** Trusted operator-selected OpenAI-compatible request extensions. */
|
|
@@ -26,6 +32,16 @@ interface ResolvedAssistantModelBase {
|
|
|
26
32
|
readonly turnsPerDay: number;
|
|
27
33
|
readonly mintsPerDay: number;
|
|
28
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Platform spend accounting, present only when someone other than the customer is paying. Passed
|
|
37
|
+
* through to `SurfaceBudgetBounds` unchanged, which is why the shape is declared identically
|
|
38
|
+
* here rather than imported — the two contracts stay independent by design.
|
|
39
|
+
*/
|
|
40
|
+
readonly spend?: {
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly units: number;
|
|
43
|
+
readonly allowance: number;
|
|
44
|
+
};
|
|
29
45
|
};
|
|
30
46
|
}
|
|
31
47
|
export type ResolvedAssistantModel = ResolvedAssistantModelBase & ({
|
|
@@ -17,11 +17,13 @@ export * from './assistant-transcript.js';
|
|
|
17
17
|
export * from './assistant-view-availability.js';
|
|
18
18
|
export * from './elevation.js';
|
|
19
19
|
export * from './elevation-store.js';
|
|
20
|
+
export * from './embed-operator-view.js';
|
|
20
21
|
export * from './embed-script.js';
|
|
21
22
|
export * from './embed-store.js';
|
|
22
23
|
export * from './in-memory-assistant-appearance-store.js';
|
|
23
24
|
export * from './in-memory-elevation-store.js';
|
|
24
25
|
export * from './in-memory-embed-store.js';
|
|
26
|
+
export * from './managed-spend.js';
|
|
25
27
|
export * from './public-configuration.js';
|
|
26
28
|
export * from './public-session.js';
|
|
27
29
|
export * from './public-surface.js';
|
|
@@ -17,11 +17,13 @@ export * from './assistant-transcript.js';
|
|
|
17
17
|
export * from './assistant-view-availability.js';
|
|
18
18
|
export * from './elevation.js';
|
|
19
19
|
export * from './elevation-store.js';
|
|
20
|
+
export * from './embed-operator-view.js';
|
|
20
21
|
export * from './embed-script.js';
|
|
21
22
|
export * from './embed-store.js';
|
|
22
23
|
export * from './in-memory-assistant-appearance-store.js';
|
|
23
24
|
export * from './in-memory-elevation-store.js';
|
|
24
25
|
export * from './in-memory-embed-store.js';
|
|
26
|
+
export * from './managed-spend.js';
|
|
25
27
|
export * from './public-configuration.js';
|
|
26
28
|
export * from './public-session.js';
|
|
27
29
|
export * from './public-surface.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AdmissionEnvelope, type DailyCounterStore } from '@noodle-borg/admission-limits/portable';
|
|
2
2
|
import { type PublicEmbedRecord, type PublicEmbedStore } from './embed-store.js';
|
|
3
3
|
import type { PublicSurface } from './public-surface.js';
|
|
4
4
|
import { type SurfaceBudgetBounds } from './surface-budget.js';
|
|
@@ -43,6 +43,13 @@ export interface PublicSessionRequest {
|
|
|
43
43
|
* be parsed, in which case the surface tier alone applies: fairness degrades, solvency does not.
|
|
44
44
|
*/
|
|
45
45
|
readonly addressBucket?: string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* The browser's own visitor identifier (admission tier 4), unvalidated exactly like `embedId`:
|
|
48
|
+
* it arrives from the request body, so the decision owns its parse rather than the transport.
|
|
49
|
+
* Client-supplied and therefore rotatable, so it bounds fairness and never abuse — the address
|
|
50
|
+
* tier underneath is the one that cannot be rotated.
|
|
51
|
+
*/
|
|
52
|
+
readonly visitorId?: unknown;
|
|
46
53
|
}
|
|
47
54
|
export type PublicSessionResult = {
|
|
48
55
|
readonly ok: true;
|
|
@@ -54,6 +61,12 @@ export type PublicSessionResult = {
|
|
|
54
61
|
readonly status: number;
|
|
55
62
|
readonly code: string;
|
|
56
63
|
readonly message: string;
|
|
64
|
+
/**
|
|
65
|
+
* The surface this refusal belongs to, once it is known. Present for every refusal an operator
|
|
66
|
+
* needs to see — out of budget, address at its ceiling, origin off the live allowlist — and
|
|
67
|
+
* absent for the ones that cannot be attributed to a tenant at all, which are prober noise.
|
|
68
|
+
*/
|
|
69
|
+
readonly embed?: PublicEmbedRecord;
|
|
57
70
|
};
|
|
58
71
|
export declare function mintPublicSession(request: PublicSessionRequest, envelope: AdmissionEnvelope, ports: PublicSessionPorts): Promise<PublicSessionResult>;
|
|
59
72
|
//# sourceMappingURL=public-session.d.ts.map
|