@runuai/host 0.9.163 → 0.9.164
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/lib/agent-cli.ts +9 -5
- package/lib/agents/claude.ts +2 -1
- package/lib/agents/codex.ts +2 -17
- package/lib/agents/grok.ts +2 -1
- package/lib/agents/types.ts +7 -0
- package/lib/engine-accounts.ts +12 -2
- package/lib/orchestrator.ts +45 -21
- package/package.json +9 -1
- package/src/engine-models.ts +31 -0
- package/src/engine-usage.ts +104 -0
- package/src/main.ts +8 -0
- package/src/protocol.ts +10 -0
package/lib/agent-cli.ts
CHANGED
|
@@ -321,6 +321,7 @@ async function main() {
|
|
|
321
321
|
const handleOf = (v) => (typeof v === "string" && v.startsWith("@") ? v.slice(1) : v);
|
|
322
322
|
const launch = (a) => a.kind + (a.model ? "/" + a.model : "") + (a.effort ? "/" + a.effort : "");
|
|
323
323
|
if (action === "list") { out(await api("GET", "/api/agent/crew")); return; }
|
|
324
|
+
if (action === "status") { out(await api("GET", "/api/agent/crew-status")); return; }
|
|
324
325
|
if (action === "add") {
|
|
325
326
|
const personaId = pos[0];
|
|
326
327
|
if (!personaId || !flags.why) {
|
|
@@ -332,14 +333,15 @@ async function main() {
|
|
|
332
333
|
if (flags.engine) body.kind = flags.engine;
|
|
333
334
|
if (flags.model) body.model = flags.model;
|
|
334
335
|
if (flags.effort) body.effort = flags.effort;
|
|
336
|
+
if (flags.account) body.account = flags.account === "default" ? null : flags.account;
|
|
335
337
|
const r = await api("POST", "/api/agent/crew", body);
|
|
336
338
|
out("added @" + r.agent.id + " (" + r.agent.label + ") on " + launch(r.agent) + ". It stays idle until you dispatch work to it.");
|
|
337
339
|
return;
|
|
338
340
|
}
|
|
339
341
|
if (action === "model") {
|
|
340
342
|
const id = handleOf(pos[0] || "");
|
|
341
|
-
if (!id || !flags.why || !(flags.model || flags.engine || flags.effort)) {
|
|
342
|
-
console.error('uai: crew model needs @agent, at least one of --engine, --model and --
|
|
343
|
+
if (!id || !flags.why || !(flags.model || flags.engine || flags.effort || flags.account)) {
|
|
344
|
+
console.error('uai: crew model needs @agent, at least one of --engine, --model, --effort and --account, and --why "<reason>"');
|
|
343
345
|
process.exit(1);
|
|
344
346
|
}
|
|
345
347
|
// "default" clears the setting back to the engine's own default (null).
|
|
@@ -348,6 +350,7 @@ async function main() {
|
|
|
348
350
|
if (flags.engine) body.kind = flags.engine;
|
|
349
351
|
if (flags.model) body.model = cleared(flags.model);
|
|
350
352
|
if (flags.effort) body.effort = cleared(flags.effort);
|
|
353
|
+
if (flags.account) body.account = cleared(flags.account);
|
|
351
354
|
const r = await api("PATCH", "/api/agent/crew/" + encodeURIComponent(id), body);
|
|
352
355
|
out(r.changed
|
|
353
356
|
? "@" + id + " now runs on " + launch(r.agent) + "; its session restarts after its current turn"
|
|
@@ -364,7 +367,7 @@ async function main() {
|
|
|
364
367
|
out("removed @" + id);
|
|
365
368
|
return;
|
|
366
369
|
}
|
|
367
|
-
console.error("uai: crew commands are list, add, model and remove (Astronaut only)");
|
|
370
|
+
console.error("uai: crew commands are list, status, add, model and remove (Astronaut only)");
|
|
368
371
|
process.exit(1);
|
|
369
372
|
}
|
|
370
373
|
const key = group + " " + (action || "");
|
|
@@ -570,8 +573,9 @@ async function main() {
|
|
|
570
573
|
" uai task read <taskId> (full chat of a past task you were on)",
|
|
571
574
|
' uai dispatch @agent [@agent…] "instruction" (Secretary only)',
|
|
572
575
|
" uai crew list (Astronaut only: personas, engines, models, limits, roster)",
|
|
573
|
-
|
|
574
|
-
' uai crew
|
|
576
|
+
" uai crew status (Astronaut only: each agent's engine, model, account; account usage; credits)",
|
|
577
|
+
' uai crew add <personaId> [--as <handle>] [--engine <kind>] [--model <m>] [--effort <e>] [--account <id|label|default>] --why "<reason>"',
|
|
578
|
+
' uai crew model @agent [--engine <kind>] [--model <m|default>] [--effort <e|default>] [--account <id|label|default>] --why "<reason>"',
|
|
575
579
|
' uai crew remove @agent --why "<reason>"',
|
|
576
580
|
" uai react <heart|check|x> [--msg #id] (no --msg = the message you were last handed)",
|
|
577
581
|
" uai whoami",
|
package/lib/agents/claude.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { newId } from "../ulid";
|
|
|
25
25
|
import { createAgentTransport, type LineTransport } from "./transport";
|
|
26
26
|
import { rateLimitErrorUsage, resetSecondsToEpochMs } from "./rate-limit";
|
|
27
27
|
import { register } from "./registry";
|
|
28
|
+
import { CLAUDE_MODELS } from "../../src/engine-models";
|
|
28
29
|
import {
|
|
29
30
|
DISPATCH_INPUT_SCHEMA,
|
|
30
31
|
DISPATCH_RECIPIENT_ID_PATTERN,
|
|
@@ -49,7 +50,7 @@ import type {
|
|
|
49
50
|
// resolves these aliases to the current dated snapshots, so this list is
|
|
50
51
|
// stable across point releases. UPDATE WHEN MODELS CHANGE (new family or a
|
|
51
52
|
// retired alias). Order is display order in the cloud picker.
|
|
52
|
-
|
|
53
|
+
// The model list is host-agent/src/engine-models.ts (CLAUDE_MODELS).
|
|
53
54
|
|
|
54
55
|
// Opus ("opus" alias = Opus 4.8) stays the default; "fable" (Claude
|
|
55
56
|
// Fable 5, the Mythos-class flagship) is opt-in per agent. Update
|
package/lib/agents/codex.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { attachCompatibilityKeysMatch } from "./attach-compatibility";
|
|
|
42
42
|
import { createAgentTransport, type LineTransport } from "./transport";
|
|
43
43
|
import { rateLimitErrorUsage, resetSecondsToEpochMs } from "./rate-limit";
|
|
44
44
|
import { register } from "./registry";
|
|
45
|
+
import { CODEX_MODELS } from "../../src/engine-models";
|
|
45
46
|
import type {
|
|
46
47
|
AgentEvent,
|
|
47
48
|
AgentAccountUsage,
|
|
@@ -51,23 +52,7 @@ import type {
|
|
|
51
52
|
RosterAgent,
|
|
52
53
|
} from "./types";
|
|
53
54
|
|
|
54
|
-
//
|
|
55
|
-
// machine-readable "list models" command — the canonical list is the
|
|
56
|
-
// interactive `codex` model picker (and `codex -m <slug>` / config.toml).
|
|
57
|
-
// Sourced from that picker; UPDATE WHEN MODELS CHANGE. Order = display order
|
|
58
|
-
// in the cloud picker. Legacy models remain reachable via config.toml.
|
|
59
|
-
const CODEX_MODELS = [
|
|
60
|
-
"gpt-6-astra",
|
|
61
|
-
"gpt-5.6-sol",
|
|
62
|
-
"gpt-5.6-terra",
|
|
63
|
-
"gpt-5.6-luna",
|
|
64
|
-
"gpt-5.5",
|
|
65
|
-
"gpt-5.4",
|
|
66
|
-
"gpt-5.4-mini",
|
|
67
|
-
"gpt-5.3-codex",
|
|
68
|
-
"gpt-5.3-codex-spark",
|
|
69
|
-
"gpt-5.2",
|
|
70
|
-
];
|
|
55
|
+
// The model list is host-agent/src/engine-models.ts (CODEX_MODELS).
|
|
71
56
|
|
|
72
57
|
// gpt-5.6-sol is Codex's current frontier coding model (the CLI default).
|
|
73
58
|
// Update alongside CODEX_MODELS. When an agent's model is null the adapter
|
package/lib/agents/grok.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type AgentEnvironmentProcess,
|
|
44
44
|
} from "./proc";
|
|
45
45
|
import { register } from "./registry";
|
|
46
|
+
import { GROK_MODELS } from "../../src/engine-models";
|
|
46
47
|
import type {
|
|
47
48
|
AgentEvent,
|
|
48
49
|
AgentEventHandler,
|
|
@@ -56,7 +57,7 @@ export const GROK_LOADER_LOGIN_ENV = "0";
|
|
|
56
57
|
|
|
57
58
|
// `grok models` lists what the account can run. grok-4.5 is the current
|
|
58
59
|
// default; UPDATE WHEN xAI CHANGES the lineup.
|
|
59
|
-
|
|
60
|
+
// The model list is host-agent/src/engine-models.ts (GROK_MODELS).
|
|
60
61
|
const GROK_DEFAULT_MODEL = "grok-4.5";
|
|
61
62
|
// Grok has no per-invocation reasoning-effort flag.
|
|
62
63
|
const GROK_EFFORTS: string[] = [];
|
package/lib/agents/types.ts
CHANGED
|
@@ -51,6 +51,13 @@ export const RosterAgent = z.object({
|
|
|
51
51
|
kind: AgentKind,
|
|
52
52
|
model: z.string().min(1).optional(),
|
|
53
53
|
effort: z.string().min(1).optional(), // CLI reasoning level, passed through
|
|
54
|
+
// ADR-157 addendum §7: preferred cloud-leased AI account.
|
|
55
|
+
accountId: z
|
|
56
|
+
.string()
|
|
57
|
+
.min(1)
|
|
58
|
+
.max(128)
|
|
59
|
+
.regex(/^[A-Za-z0-9_.:-]+$/)
|
|
60
|
+
.optional(),
|
|
54
61
|
defaultPrompt: z.string().optional(), // persona
|
|
55
62
|
initialPrompt: z.string().optional(), // first-turn instructions, this task only
|
|
56
63
|
skills: z.array(RosterSkill).optional(), // ADR-046 reference material
|
package/lib/engine-accounts.ts
CHANGED
|
@@ -558,7 +558,7 @@ export function noteEngineAccountUsed(id: string, now: number = Date.now()): voi
|
|
|
558
558
|
*/
|
|
559
559
|
export function selectAccount(
|
|
560
560
|
accounts: ResolvedEngineAccount[],
|
|
561
|
-
opts: { exclude?: Set<string>; now?: number } = {},
|
|
561
|
+
opts: { exclude?: Set<string>; now?: number; preferredId?: string } = {},
|
|
562
562
|
): ResolvedEngineAccount | null {
|
|
563
563
|
const now = opts.now ?? Date.now();
|
|
564
564
|
const exclude = opts.exclude ?? new Set<string>();
|
|
@@ -566,6 +566,10 @@ export function selectAccount(
|
|
|
566
566
|
if (candidates.length === 0) return null;
|
|
567
567
|
const lru = (a: ResolvedEngineAccount): number => state(a.id).lastUsedAt;
|
|
568
568
|
const healthy = candidates.filter((a) => accountCooldownUntil(a.id) <= now);
|
|
569
|
+
const preferred = opts.preferredId
|
|
570
|
+
? healthy.find((account) => account.id === opts.preferredId)
|
|
571
|
+
: undefined;
|
|
572
|
+
if (preferred) return preferred;
|
|
569
573
|
// Manage-v1: the list arrives in PRIORITY order (default slot, then extras
|
|
570
574
|
// by position) — the first healthy account wins, so the operator's
|
|
571
575
|
// ordering decides which account a task uses and which are fallbacks.
|
|
@@ -585,6 +589,8 @@ export function pickEngineAccount(
|
|
|
585
589
|
exclude?: Set<string>;
|
|
586
590
|
seams?: Partial<AccountSeams>;
|
|
587
591
|
now?: number;
|
|
592
|
+
/** One agent's durable preference; cooling or absent falls through. */
|
|
593
|
+
preferredId?: string;
|
|
588
594
|
/** ADR-131 D3: the task's scope; default = the host's own accounts. */
|
|
589
595
|
scope?: string;
|
|
590
596
|
} = {},
|
|
@@ -594,7 +600,11 @@ export function pickEngineAccount(
|
|
|
594
600
|
opts.seams ?? {},
|
|
595
601
|
opts.scope ?? HOST_ACCOUNT_SCOPE,
|
|
596
602
|
);
|
|
597
|
-
return selectAccount(accounts, {
|
|
603
|
+
return selectAccount(accounts, {
|
|
604
|
+
exclude: opts.exclude,
|
|
605
|
+
now: opts.now,
|
|
606
|
+
preferredId: opts.preferredId,
|
|
607
|
+
});
|
|
598
608
|
}
|
|
599
609
|
|
|
600
610
|
/**
|
package/lib/orchestrator.ts
CHANGED
|
@@ -166,6 +166,7 @@ import {
|
|
|
166
166
|
selectAccount,
|
|
167
167
|
type ResolvedEngineAccount,
|
|
168
168
|
} from "./engine-accounts";
|
|
169
|
+
import { buildEngineUsageFrame } from "../src/engine-usage";
|
|
169
170
|
import {
|
|
170
171
|
ENGINE_ACCOUNT_HOMES,
|
|
171
172
|
EngineCredentialWriteRefused,
|
|
@@ -1746,11 +1747,19 @@ export class Orchestrator {
|
|
|
1746
1747
|
channel: Channel,
|
|
1747
1748
|
kind: string,
|
|
1748
1749
|
exclude?: Set<string>,
|
|
1750
|
+
preferredId?: string,
|
|
1749
1751
|
): ResolvedEngineAccount | null {
|
|
1750
1752
|
if (!this.isLeasedChannel(channel)) {
|
|
1751
|
-
return pickEngineAccount(kind, {
|
|
1753
|
+
return pickEngineAccount(kind, {
|
|
1754
|
+
scope: channel.engineAccountScope,
|
|
1755
|
+
exclude,
|
|
1756
|
+
preferredId,
|
|
1757
|
+
});
|
|
1752
1758
|
}
|
|
1753
|
-
return selectAccount(this.engineAccounts(channel, kind), {
|
|
1759
|
+
return selectAccount(this.engineAccounts(channel, kind), {
|
|
1760
|
+
exclude,
|
|
1761
|
+
preferredId,
|
|
1762
|
+
});
|
|
1754
1763
|
}
|
|
1755
1764
|
|
|
1756
1765
|
private noteProvisionMutation(
|
|
@@ -2789,6 +2798,7 @@ export class Orchestrator {
|
|
|
2789
2798
|
if (this.isLeasedChannel(channel)) return { ...floor, ...base };
|
|
2790
2799
|
const account = pickEngineAccount(agent.kind, {
|
|
2791
2800
|
scope: channel.engineAccountScope,
|
|
2801
|
+
preferredId: agent.accountId,
|
|
2792
2802
|
});
|
|
2793
2803
|
if (!account) {
|
|
2794
2804
|
channel.accountByAgent.delete(agent.id);
|
|
@@ -3381,8 +3391,14 @@ export class Orchestrator {
|
|
|
3381
3391
|
account.engineKind === kind && isLeasedEngineAccount(account),
|
|
3382
3392
|
);
|
|
3383
3393
|
const bound = channel.accountByAgent.get(agentId);
|
|
3394
|
+
const preferredId = channel.roster.find(
|
|
3395
|
+
(candidate) => candidate.id === agentId,
|
|
3396
|
+
)?.accountId;
|
|
3384
3397
|
const kept = accounts.find((account) => account.credentialId === bound);
|
|
3385
|
-
|
|
3398
|
+
// Without a pin, preserve the live binding. With one, every fresh session
|
|
3399
|
+
// re-evaluates it: a cooling/missing pin falls through now, and wins again
|
|
3400
|
+
// automatically after its bench lifts.
|
|
3401
|
+
if (!preferredId && kept) return kept;
|
|
3386
3402
|
const picked = selectAccount(
|
|
3387
3403
|
accounts.map((account) => ({
|
|
3388
3404
|
id: account.credentialId,
|
|
@@ -3393,6 +3409,7 @@ export class Orchestrator {
|
|
|
3393
3409
|
authKind: account.authKind,
|
|
3394
3410
|
execEnv: {},
|
|
3395
3411
|
})),
|
|
3412
|
+
{ preferredId },
|
|
3396
3413
|
);
|
|
3397
3414
|
return accounts.find((account) => account.credentialId === picked?.id);
|
|
3398
3415
|
}
|
|
@@ -5002,6 +5019,24 @@ export class Orchestrator {
|
|
|
5002
5019
|
if (!identity || identity.subject !== "account") break;
|
|
5003
5020
|
const agent = channel.roster.find((candidate) => candidate.id === agentId);
|
|
5004
5021
|
if (!agent || !isEngineUsageKind(agent.kind)) break;
|
|
5022
|
+
// The session's own revision: a refreshed lease may already hold a
|
|
5023
|
+
// newer one, and its variable name says nothing about this session.
|
|
5024
|
+
const leasedAccount = leasedEnginesFor(channel.taskId)?.accounts.find(
|
|
5025
|
+
(candidate): candidate is LeasedEngineAccount =>
|
|
5026
|
+
candidate.credentialId === identity.credentialId &&
|
|
5027
|
+
candidate.revision === identity.revision &&
|
|
5028
|
+
isLeasedEngineAccount(candidate),
|
|
5029
|
+
);
|
|
5030
|
+
const frame = buildEngineUsageFrame({
|
|
5031
|
+
reportId: newId(),
|
|
5032
|
+
taskId: channel.taskId,
|
|
5033
|
+
agentId,
|
|
5034
|
+
engine: agent.kind,
|
|
5035
|
+
credentialId: identity.credentialId,
|
|
5036
|
+
revision: identity.revision,
|
|
5037
|
+
usage: event.usage,
|
|
5038
|
+
leasedAccount,
|
|
5039
|
+
});
|
|
5005
5040
|
|
|
5006
5041
|
const coverage = event.usage.coverage;
|
|
5007
5042
|
if (event.usage.status === "limited") {
|
|
@@ -5048,6 +5083,8 @@ export class Orchestrator {
|
|
|
5048
5083
|
taskId: channel.taskId,
|
|
5049
5084
|
engine: agent.kind,
|
|
5050
5085
|
credentialId: identity.credentialId,
|
|
5086
|
+
billing: frame.billing,
|
|
5087
|
+
revision: frame.revision,
|
|
5051
5088
|
status: event.usage.status,
|
|
5052
5089
|
resetsAt: event.usage.resetsAt,
|
|
5053
5090
|
usingOverage: event.usage.usingOverage,
|
|
@@ -5062,24 +5099,7 @@ export class Orchestrator {
|
|
|
5062
5099
|
) {
|
|
5063
5100
|
break;
|
|
5064
5101
|
}
|
|
5065
|
-
const delivered = this.emitEngineUsage(
|
|
5066
|
-
kind: "engine.usage",
|
|
5067
|
-
reportId: newId(),
|
|
5068
|
-
taskId: channel.taskId,
|
|
5069
|
-
agentId,
|
|
5070
|
-
engine: agent.kind,
|
|
5071
|
-
credentialId: identity.credentialId,
|
|
5072
|
-
status: event.usage.status,
|
|
5073
|
-
...(event.usage.resetsAt === undefined
|
|
5074
|
-
? {}
|
|
5075
|
-
: { resetsAt: event.usage.resetsAt }),
|
|
5076
|
-
...(event.usage.usingOverage === undefined
|
|
5077
|
-
? {}
|
|
5078
|
-
: { usingOverage: event.usage.usingOverage }),
|
|
5079
|
-
windows: event.usage.windows,
|
|
5080
|
-
source: event.usage.source,
|
|
5081
|
-
observedAt: event.usage.observedAt,
|
|
5082
|
-
});
|
|
5102
|
+
const delivered = this.emitEngineUsage(frame);
|
|
5083
5103
|
// A disconnected bridge returns false. Do not suppress the next
|
|
5084
5104
|
// identical observation unless this one actually reached the socket.
|
|
5085
5105
|
if (delivered) {
|
|
@@ -5779,6 +5799,7 @@ export class Orchestrator {
|
|
|
5779
5799
|
channel,
|
|
5780
5800
|
agent.kind,
|
|
5781
5801
|
current ? new Set([current]) : undefined,
|
|
5802
|
+
agent.accountId,
|
|
5782
5803
|
);
|
|
5783
5804
|
if (!next) return false;
|
|
5784
5805
|
|
|
@@ -6646,6 +6667,9 @@ function rosterLaunchChangeReason(
|
|
|
6646
6667
|
if (previous.kind !== next.kind) changed.push("engine");
|
|
6647
6668
|
if ((previous.model ?? "") !== (next.model ?? "")) changed.push("model");
|
|
6648
6669
|
if ((previous.effort ?? "") !== (next.effort ?? "")) changed.push("effort");
|
|
6670
|
+
if ((previous.accountId ?? "") !== (next.accountId ?? "")) {
|
|
6671
|
+
changed.push("account");
|
|
6672
|
+
}
|
|
6649
6673
|
// ADR-157 §3: the session holds a token minted for its generation, so a new
|
|
6650
6674
|
// one is a new agent under the handle (or one an older cloud release lost),
|
|
6651
6675
|
// and only a fresh launch carries a token the cloud accepts.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runuai/host",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.164",
|
|
4
4
|
"description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Uai Tech <team@runuai.com>",
|
|
@@ -72,6 +72,14 @@
|
|
|
72
72
|
"./project-env-wording": {
|
|
73
73
|
"types": "./src/project-env-wording.ts",
|
|
74
74
|
"default": "./src/project-env-wording.ts"
|
|
75
|
+
},
|
|
76
|
+
"./engine-models": {
|
|
77
|
+
"types": "./src/engine-models.ts",
|
|
78
|
+
"default": "./src/engine-models.ts"
|
|
79
|
+
},
|
|
80
|
+
"./engine-usage": {
|
|
81
|
+
"types": "./src/engine-usage.ts",
|
|
82
|
+
"default": "./src/engine-usage.ts"
|
|
75
83
|
}
|
|
76
84
|
},
|
|
77
85
|
"scripts": {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The models each engine adapter offers, by the id the runner passes its
|
|
3
|
+
* CLI: one list per engine, read by the adapters (`supportedModels`) and by
|
|
4
|
+
* the cloud's model guide (lib/model-guide.ts), whose test fails when a model
|
|
5
|
+
* here has no guide entry. Pure data: no runtime imports.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Claude Code's aliases, which follow its own model table (2.1.307:
|
|
9
|
+
// fable = Claude Fable 5.1, opus = Opus 5.5, sonnet = Sonnet 5.5,
|
|
10
|
+
// haiku = Haiku 5.5). Update alongside the CLI.
|
|
11
|
+
export const CLAUDE_MODELS: readonly string[] = ["fable", "opus", "sonnet", "haiku"];
|
|
12
|
+
|
|
13
|
+
// Codex model slugs accepted via `-c model=<slug>`. Hardcoded: Codex has no
|
|
14
|
+
// machine-readable "list models" command — the canonical list is the
|
|
15
|
+
// interactive `codex` model picker (and `codex -m <slug>` / config.toml).
|
|
16
|
+
// Sourced from that picker; UPDATE WHEN MODELS CHANGE. Order = display order
|
|
17
|
+
// in the cloud picker. Legacy models remain reachable via config.toml.
|
|
18
|
+
export const CODEX_MODELS: readonly string[] = [
|
|
19
|
+
"gpt-6-astra",
|
|
20
|
+
"gpt-5.6-sol",
|
|
21
|
+
"gpt-5.6-terra",
|
|
22
|
+
"gpt-5.6-luna",
|
|
23
|
+
"gpt-5.5",
|
|
24
|
+
"gpt-5.4",
|
|
25
|
+
"gpt-5.4-mini",
|
|
26
|
+
"gpt-5.3-codex",
|
|
27
|
+
"gpt-5.3-codex-spark",
|
|
28
|
+
"gpt-5.2",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const GROK_MODELS: readonly string[] = ["grok-4.5"];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the secret-free account usage report sent to Uai Cloud.
|
|
3
|
+
*
|
|
4
|
+
* A leased env account carries both a variable name and its secret value in
|
|
5
|
+
* runner memory. Billing is deliberately inferred from the NAME alone. Some
|
|
6
|
+
* file-backed accounts use the same `auth.json` name for a subscription and
|
|
7
|
+
* an API key, so they carry no hint.
|
|
8
|
+
*
|
|
9
|
+
* The hint names the revision it was read from, and is given only when that is
|
|
10
|
+
* the revision the reporting session runs on: after a re-paste, a lease
|
|
11
|
+
* refresh can hand the runner newer material than an agent is still using.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EngineUsageFrame, EngineUsageWindow } from "./protocol";
|
|
15
|
+
|
|
16
|
+
type EngineUsageBilling = NonNullable<EngineUsageFrame["billing"]>;
|
|
17
|
+
|
|
18
|
+
export interface LeasedBillingAccount {
|
|
19
|
+
credentialId: string;
|
|
20
|
+
revision: string;
|
|
21
|
+
engineKind: string;
|
|
22
|
+
authKind: "env" | "config-dir";
|
|
23
|
+
env: Readonly<Record<string, string>> | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface EngineUsageObservation {
|
|
27
|
+
status: EngineUsageFrame["status"];
|
|
28
|
+
resetsAt?: number;
|
|
29
|
+
usingOverage?: boolean;
|
|
30
|
+
windows: EngineUsageWindow[];
|
|
31
|
+
source: EngineUsageFrame["source"];
|
|
32
|
+
observedAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const BILLING_BY_VARIABLE: Readonly<
|
|
36
|
+
Record<string, Readonly<Record<string, EngineUsageBilling>>>
|
|
37
|
+
> = Object.freeze({
|
|
38
|
+
claude: Object.freeze({
|
|
39
|
+
// Capture files every pasted Claude secret that is NOT `sk-ant-api…` under
|
|
40
|
+
// this name (engine-accounts.ts `pastedSecretMaterial`), so the name alone
|
|
41
|
+
// also covers a token whose prefix told capture nothing, which the cloud
|
|
42
|
+
// left unknown. Subscription is still right for a report: usage is read
|
|
43
|
+
// from a session that worked, and Claude Code authenticates this variable
|
|
44
|
+
// only as an OAuth (subscription) token, so a session that ran on it was
|
|
45
|
+
// one. Do not "correct" it to unknown.
|
|
46
|
+
CLAUDE_CODE_OAUTH_TOKEN: "subscription",
|
|
47
|
+
ANTHROPIC_API_KEY: "api-key",
|
|
48
|
+
}),
|
|
49
|
+
cursor: Object.freeze({ CURSOR_API_KEY: "api-key" }),
|
|
50
|
+
grok: Object.freeze({ XAI_API_KEY: "api-key" }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** The billing lane when one leased variable name tells it unambiguously. */
|
|
54
|
+
export function billingFromLeasedVariableName(
|
|
55
|
+
account: LeasedBillingAccount | null | undefined,
|
|
56
|
+
): EngineUsageBilling | undefined {
|
|
57
|
+
if (!account || account.authKind !== "env" || account.env === null) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const names = Object.keys(account.env);
|
|
61
|
+
if (names.length !== 1) return undefined;
|
|
62
|
+
return BILLING_BY_VARIABLE[account.engineKind]?.[names[0]!];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Construct the exact wire frame for one adapter usage observation.
|
|
66
|
+
* `revision` is the one the reporting session runs on; a leased account at
|
|
67
|
+
* any other revision gives no hint. */
|
|
68
|
+
export function buildEngineUsageFrame(input: {
|
|
69
|
+
reportId: string;
|
|
70
|
+
taskId: string;
|
|
71
|
+
agentId: string;
|
|
72
|
+
engine: EngineUsageFrame["engine"];
|
|
73
|
+
credentialId: string;
|
|
74
|
+
revision?: string;
|
|
75
|
+
usage: EngineUsageObservation;
|
|
76
|
+
leasedAccount?: LeasedBillingAccount | null;
|
|
77
|
+
}): EngineUsageFrame {
|
|
78
|
+
const { usage } = input;
|
|
79
|
+
const account =
|
|
80
|
+
input.revision !== undefined &&
|
|
81
|
+
input.leasedAccount?.credentialId === input.credentialId &&
|
|
82
|
+
input.leasedAccount.revision === input.revision &&
|
|
83
|
+
input.leasedAccount.engineKind === input.engine
|
|
84
|
+
? input.leasedAccount
|
|
85
|
+
: null;
|
|
86
|
+
const billing = billingFromLeasedVariableName(account);
|
|
87
|
+
return {
|
|
88
|
+
kind: "engine.usage",
|
|
89
|
+
reportId: input.reportId,
|
|
90
|
+
taskId: input.taskId,
|
|
91
|
+
agentId: input.agentId,
|
|
92
|
+
engine: input.engine,
|
|
93
|
+
credentialId: input.credentialId,
|
|
94
|
+
...(billing === undefined ? {} : { billing, revision: account!.revision }),
|
|
95
|
+
status: usage.status,
|
|
96
|
+
...(usage.resetsAt === undefined ? {} : { resetsAt: usage.resetsAt }),
|
|
97
|
+
...(usage.usingOverage === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { usingOverage: usage.usingOverage }),
|
|
100
|
+
windows: usage.windows,
|
|
101
|
+
source: usage.source,
|
|
102
|
+
observedAt: usage.observedAt,
|
|
103
|
+
};
|
|
104
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -5536,6 +5536,14 @@ function expectTaskAgents(value: unknown): TaskAgent[] {
|
|
|
5536
5536
|
// effort was previously dropped here even though the protocol + adapters
|
|
5537
5537
|
// carry it — fixed alongside skills (ADR-046).
|
|
5538
5538
|
if (typeof row.effort === "string") out.effort = row.effort;
|
|
5539
|
+
if (
|
|
5540
|
+
typeof row.accountId === "string" &&
|
|
5541
|
+
row.accountId.length > 0 &&
|
|
5542
|
+
row.accountId.length <= 128 &&
|
|
5543
|
+
/^[A-Za-z0-9_.:-]+$/.test(row.accountId)
|
|
5544
|
+
) {
|
|
5545
|
+
out.accountId = row.accountId;
|
|
5546
|
+
}
|
|
5539
5547
|
if (row.uaiProvider === "anthropic" || row.uaiProvider === "openai") {
|
|
5540
5548
|
out.uaiProvider = row.uaiProvider;
|
|
5541
5549
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -194,6 +194,14 @@ export interface EngineUsageFrame {
|
|
|
194
194
|
engine: "claude" | "codex" | "grok" | "cursor" | "opencode";
|
|
195
195
|
/** The `uai_credentials` id the agent's session runs on. */
|
|
196
196
|
credentialId: string;
|
|
197
|
+
/** How this account is paid for, when the leased variable's NAME tells the
|
|
198
|
+
* runner unambiguously. Derived only from that name, never from its value. */
|
|
199
|
+
billing?: "subscription" | "api-key";
|
|
200
|
+
/** The credential revision whose variable name gave `billing`: the one the
|
|
201
|
+
* agent's session runs on. Sent only with `billing`; an id, not a secret.
|
|
202
|
+
* The cloud records the hint only while this is still the account's active
|
|
203
|
+
* revision, so a task on an older lease cannot describe newer material. */
|
|
204
|
+
revision?: string;
|
|
197
205
|
/** `allowed`: working; `warning`: the engine says it is close to a limit;
|
|
198
206
|
* `limited`: cut off until `resetsAt` (or an unknown time). */
|
|
199
207
|
status: "allowed" | "warning" | "limited";
|
|
@@ -3121,6 +3129,8 @@ export interface TaskAgent {
|
|
|
3121
3129
|
kind: string;
|
|
3122
3130
|
model?: string;
|
|
3123
3131
|
effort?: string;
|
|
3132
|
+
/** ADR-157 addendum §7: preferred cloud-leased AI account. */
|
|
3133
|
+
accountId?: string;
|
|
3124
3134
|
defaultPrompt?: string;
|
|
3125
3135
|
initialPrompt?: string;
|
|
3126
3136
|
/** ADR-046 reference material resolved from the agent's persona + team.
|