@sellable/install 0.1.800 → 0.1.802

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.
@@ -5,6 +5,7 @@ export const ENDPOINT_MODEL_SELECTION_ALLOWLIST: Readonly<{
5
5
  "openai/gpt-6-luna",
6
6
  "anthropic/claude-sonnet-5.5",
7
7
  ];
8
+ "openai-codex": readonly ["gpt-6.1-sol"];
8
9
  }>;
9
10
  export const ENDPOINT_MODEL_REASONING_EFFORTS: readonly [
10
11
  "none",
@@ -13,14 +14,60 @@ export const ENDPOINT_MODEL_REASONING_EFFORTS: readonly [
13
14
  "high",
14
15
  "xhigh",
15
16
  ];
16
- export type EndpointModelSelection = Readonly<{
17
+ export const ENDPOINT_MODEL_MAX_FALLBACKS: 2;
18
+ export type EndpointModelStep = Readonly<{
17
19
  provider: string;
18
20
  model: string;
19
21
  reasoningEffort: (typeof ENDPOINT_MODEL_REASONING_EFFORTS)[number];
20
22
  runsApiBaseUrl?: string;
21
- fallbackPolicy?: "STOP" | "PRODUCT_DEFAULT";
22
23
  }>;
24
+ export type EndpointModelSelection = Readonly<
25
+ EndpointModelStep & {
26
+ fallbackPolicy?: "STOP" | "PRODUCT_DEFAULT";
27
+ fallbacks?: readonly EndpointModelStep[];
28
+ }
29
+ >;
30
+ export type EndpointChainFailure =
31
+ | "NATIVE_FAILED"
32
+ | "OUTPUT_CONTRACT_FAILED"
33
+ | "NOT_ADMITTED"
34
+ | "STEP_UNAVAILABLE";
35
+ export type EndpointChainDecision =
36
+ | Readonly<{
37
+ advance: true;
38
+ stepIndex: number;
39
+ selection: EndpointModelStep;
40
+ }>
41
+ | Readonly<{
42
+ advance: false;
43
+ reason:
44
+ | "INVALID"
45
+ | "NOT_ADVANCEABLE"
46
+ | "CHAIN_EXHAUSTED"
47
+ | "NO_TIME_LEFT";
48
+ }>;
23
49
  export function normalizeRunsApiBaseUrl(value: unknown): string | null;
24
50
  export function compileEndpointModelSelection(
25
51
  value: unknown
26
52
  ): EndpointModelSelection | null | "invalid";
53
+ export function endpointModelSelectionChain(
54
+ selection: unknown
55
+ ): EndpointModelStep[];
56
+ export function endpointModelSelectionSource(
57
+ selection: { provider: string; runsApiBaseUrl?: string } | null | undefined
58
+ ): "default" | "openrouter" | "codex" | "custom";
59
+ export const ENDPOINT_CHAIN_STEP_ADMISSION_MS: number;
60
+ export const ENDPOINT_CHAIN_MIN_STEP_MS: number;
61
+ export const ENDPOINT_CHAIN_ADVANCE_FAILURES: readonly EndpointChainFailure[];
62
+ export function nextEndpointChainStep(
63
+ chain: readonly EndpointModelStep[],
64
+ history: readonly string[],
65
+ failure: string,
66
+ remainingMs?: number
67
+ ): EndpointChainDecision;
68
+ export function endpointChainStepAdmissionDeadline(input: {
69
+ startedAtMs: number;
70
+ deadlineMs: number;
71
+ stepIndex: number;
72
+ stepCount: number;
73
+ }): number;
@@ -8,6 +8,9 @@ export const ENDPOINT_MODEL_SELECTION_ALLOWLIST = Object.freeze({
8
8
  "openai/gpt-6-luna",
9
9
  "anthropic/claude-sonnet-5.5",
10
10
  ]),
11
+ // The Agent's own Codex model (model-auth-reconciler.mjs), served by Hermes
12
+ // per run from the machine's Codex subscription pool.
13
+ "openai-codex": Object.freeze(["gpt-6.1-sol"]),
11
14
  });
12
15
  export const ENDPOINT_MODEL_REASONING_EFFORTS = Object.freeze([
13
16
  "none",
@@ -16,6 +19,7 @@ export const ENDPOINT_MODEL_REASONING_EFFORTS = Object.freeze([
16
19
  "high",
17
20
  "xhigh",
18
21
  ]);
22
+ export const ENDPOINT_MODEL_MAX_FALLBACKS = 2;
19
23
 
20
24
  export function normalizeRunsApiBaseUrl(value) {
21
25
  if (
@@ -45,39 +49,33 @@ export function normalizeRunsApiBaseUrl(value) {
45
49
  }
46
50
  }
47
51
 
48
- export function compileEndpointModelSelection(value) {
49
- if (value === undefined || value === null) return null;
50
- if (typeof value !== "object" || Array.isArray(value)) return "invalid";
52
+ // One model: provider, model, effort and, for a custom Runs API, its URL.
53
+ function compileEndpointModelStep(value, extraKeys = []) {
54
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
51
55
  const custom = Object.hasOwn(value, "runsApiBaseUrl");
52
- const hasFallbackPolicy = Object.hasOwn(value, "fallbackPolicy");
53
56
  if (
54
57
  Object.keys(value)
55
- .filter((key) => key !== "fallbackPolicy")
58
+ .filter((key) => !extraKeys.includes(key))
56
59
  .sort()
57
60
  .join(",") !==
58
61
  (custom
59
62
  ? "model,provider,reasoningEffort,runsApiBaseUrl"
60
63
  : "model,provider,reasoningEffort")
61
64
  )
62
- return "invalid";
63
- if (
64
- hasFallbackPolicy &&
65
- !["STOP", "PRODUCT_DEFAULT"].includes(value.fallbackPolicy)
66
- )
67
- return "invalid";
65
+ return null;
68
66
  const { provider, model, reasoningEffort } = value;
69
67
  if (
70
68
  typeof provider !== "string" ||
71
69
  typeof model !== "string" ||
72
70
  !ENDPOINT_MODEL_REASONING_EFFORTS.includes(reasoningEffort)
73
71
  )
74
- return "invalid";
72
+ return null;
75
73
  if (
76
74
  !custom &&
77
75
  (!Object.hasOwn(ENDPOINT_MODEL_SELECTION_ALLOWLIST, provider) ||
78
76
  !ENDPOINT_MODEL_SELECTION_ALLOWLIST[provider].includes(model))
79
77
  )
80
- return "invalid";
78
+ return null;
81
79
  const runsApiBaseUrl = custom
82
80
  ? normalizeRunsApiBaseUrl(value.runsApiBaseUrl)
83
81
  : null;
@@ -87,12 +85,150 @@ export function compileEndpointModelSelection(value) {
87
85
  !/^[a-z][a-z0-9_-]{0,63}$/.test(provider) ||
88
86
  !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/.test(model))
89
87
  )
90
- return "invalid";
88
+ return null;
91
89
  return Object.freeze({
92
90
  provider,
93
91
  model,
94
92
  reasoningEffort,
95
93
  ...(custom ? { runsApiBaseUrl } : {}),
94
+ });
95
+ }
96
+
97
+ const sameStep = (left, right) =>
98
+ left.provider === right.provider &&
99
+ left.model === right.model &&
100
+ left.reasoningEffort === right.reasoningEffort &&
101
+ left.runsApiBaseUrl === right.runsApiBaseUrl;
102
+
103
+ /**
104
+ * The top-level fields are the endpoint's first model. An optional ordered
105
+ * `fallbacks` list holds up to two more, each a full model choice of its own;
106
+ * `fallbackPolicy` applies only after every model in the chain has failed.
107
+ */
108
+ export function compileEndpointModelSelection(value) {
109
+ if (value === undefined || value === null) return null;
110
+ if (typeof value !== "object" || Array.isArray(value)) return "invalid";
111
+ const primary = compileEndpointModelStep(value, [
112
+ "fallbackPolicy",
113
+ "fallbacks",
114
+ ]);
115
+ if (!primary) return "invalid";
116
+ const hasFallbackPolicy = Object.hasOwn(value, "fallbackPolicy");
117
+ if (
118
+ hasFallbackPolicy &&
119
+ !["STOP", "PRODUCT_DEFAULT"].includes(value.fallbackPolicy)
120
+ )
121
+ return "invalid";
122
+ let fallbacks = null;
123
+ if (Object.hasOwn(value, "fallbacks")) {
124
+ if (
125
+ !Array.isArray(value.fallbacks) ||
126
+ value.fallbacks.length < 1 ||
127
+ value.fallbacks.length > ENDPOINT_MODEL_MAX_FALLBACKS
128
+ )
129
+ return "invalid";
130
+ fallbacks = value.fallbacks.map((step) => compileEndpointModelStep(step));
131
+ if (
132
+ fallbacks.some(
133
+ (step, index) =>
134
+ !step ||
135
+ [primary, ...fallbacks.slice(0, index)].some((earlier) =>
136
+ sameStep(earlier, step)
137
+ )
138
+ )
139
+ )
140
+ return "invalid";
141
+ }
142
+ return Object.freeze({
143
+ ...primary,
96
144
  ...(hasFallbackPolicy ? { fallbackPolicy: value.fallbackPolicy } : {}),
145
+ ...(fallbacks ? { fallbacks: Object.freeze(fallbacks) } : {}),
97
146
  });
98
147
  }
148
+
149
+ /** The models a run tries, in order; empty means the Agent's own default. */
150
+ export function endpointModelSelectionChain(selection) {
151
+ const compiled = compileEndpointModelSelection(selection);
152
+ if (!compiled || compiled === "invalid") return [];
153
+ const { fallbacks = [], fallbackPolicy: _policy, ...primary } = compiled;
154
+ return [Object.freeze(primary), ...fallbacks];
155
+ }
156
+
157
+ /** The settings source a model belongs to: Agent default, OpenRouter, Codex or a custom Runs API. */
158
+ export function endpointModelSelectionSource(selection) {
159
+ if (!selection) return "default";
160
+ if (selection.runsApiBaseUrl) return "custom";
161
+ return selection.provider === "openai-codex" ? "codex" : "openrouter";
162
+ }
163
+
164
+ // A step that cannot be admitted waits this long at most before the next
165
+ // model takes the run. Claude Code runs take 9-15 s, so a busy slot normally
166
+ // frees well inside it.
167
+ export const ENDPOINT_CHAIN_STEP_ADMISSION_MS = 30_000;
168
+ // Starting another model with less time than this left cannot finish it.
169
+ export const ENDPOINT_CHAIN_MIN_STEP_MS = 5_000;
170
+ // A step that failed in one of these ways produced no output, so another
171
+ // model can write without risking a second draft or send.
172
+ export const ENDPOINT_CHAIN_ADVANCE_FAILURES = Object.freeze([
173
+ "NATIVE_FAILED", // the run failed with no output (incl. Claude Code usage limit or sign-in refusals)
174
+ "OUTPUT_CONTRACT_FAILED", // its output failed the product contract and was not used
175
+ "NOT_ADMITTED", // the service stayed busy (429/503) for the step's admission budget
176
+ "STEP_UNAVAILABLE", // this machine could not open the step's connection; nothing was sent
177
+ ]);
178
+
179
+ /**
180
+ * Decide whether a run moves to the next model of its chain. `history` holds
181
+ * the failure classes of the steps already left behind; `failure` is how the
182
+ * current step ended. Anything not listed in ENDPOINT_CHAIN_ADVANCE_FAILURES
183
+ * (an ambiguous submit, a lost stream, a stop or cancel, a late completion,
184
+ * success including a SUPPRESS decision, a timeout) ends the run.
185
+ */
186
+ export function nextEndpointChainStep(
187
+ chain,
188
+ history,
189
+ failure,
190
+ remainingMs = Number.POSITIVE_INFINITY
191
+ ) {
192
+ if (
193
+ !Array.isArray(chain) ||
194
+ chain.length < 1 ||
195
+ chain.length > ENDPOINT_MODEL_MAX_FALLBACKS + 1 ||
196
+ !Array.isArray(history) ||
197
+ history.length >= chain.length ||
198
+ history.some((item) => !ENDPOINT_CHAIN_ADVANCE_FAILURES.includes(item))
199
+ )
200
+ return Object.freeze({ advance: false, reason: "INVALID" });
201
+ if (!ENDPOINT_CHAIN_ADVANCE_FAILURES.includes(failure))
202
+ return Object.freeze({ advance: false, reason: "NOT_ADVANCEABLE" });
203
+ const stepIndex = history.length + 1;
204
+ if (stepIndex >= chain.length)
205
+ return Object.freeze({ advance: false, reason: "CHAIN_EXHAUSTED" });
206
+ if (!(remainingMs >= ENDPOINT_CHAIN_MIN_STEP_MS))
207
+ return Object.freeze({ advance: false, reason: "NO_TIME_LEFT" });
208
+ return Object.freeze({
209
+ advance: true,
210
+ stepIndex,
211
+ selection: chain[stepIndex],
212
+ });
213
+ }
214
+
215
+ /**
216
+ * When a busy step stops waiting for admission. Every step but the last gets
217
+ * an equal share of the time left, at most ENDPOINT_CHAIN_STEP_ADMISSION_MS;
218
+ * the last step, like a single model, waits until the attempt deadline.
219
+ */
220
+ export function endpointChainStepAdmissionDeadline({
221
+ startedAtMs,
222
+ deadlineMs,
223
+ stepIndex,
224
+ stepCount,
225
+ }) {
226
+ if (stepIndex >= stepCount - 1) return deadlineMs;
227
+ const share = Math.floor(
228
+ (deadlineMs - startedAtMs) / (stepCount - stepIndex)
229
+ );
230
+ return Math.min(
231
+ deadlineMs,
232
+ startedAtMs + Math.max(0, Math.min(ENDPOINT_CHAIN_STEP_ADMISSION_MS, share))
233
+ );
234
+ }
@@ -1,7 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { pathToFileURL } from "node:url";
4
- import { compileEndpointModelSelection } from "./endpoint-model-selection.mjs";
4
+ import {
5
+ compileEndpointModelSelection,
6
+ endpointModelSelectionChain,
7
+ endpointModelSelectionSource,
8
+ } from "./endpoint-model-selection.mjs";
5
9
  import { createHermesRunsClient } from "./hermes-runs-client.mjs";
6
10
  import {
7
11
  createConfiguredRunsClient,
@@ -132,8 +136,12 @@ export async function verifyRunsApiSelection(
132
136
  !/^[A-Za-z0-9_-]{1,128}$/.test(request.profileId ?? "")
133
137
  )
134
138
  return result("NOT_CONFIGURED");
135
- const selection = compileEndpointModelSelection(request.modelSelection);
136
- if (selection === "invalid") return result("NOT_CONFIGURED");
139
+ const compiled = compileEndpointModelSelection(request.modelSelection);
140
+ if (compiled === "invalid") return result("NOT_CONFIGURED");
141
+ // A fallback chain is verified one step per call. The control plane sends
142
+ // the primary step's check the whole chain, so a machine whose compiler
143
+ // cannot run a chain refuses it here and no chain is saved for it.
144
+ const selection = endpointModelSelectionChain(compiled)[0] ?? null;
137
145
  let verificationWorkKind = "STANDARD";
138
146
  if (selection?.runsApiBaseUrl) {
139
147
  const connection = runsApiConnectionsForRuntime(
@@ -156,7 +164,7 @@ export async function verifyRunsApiSelection(
156
164
  const readiness = await health(request, { env });
157
165
  if (readiness.state !== "READY") return readiness;
158
166
  }
159
- } else if (request.connectionId !== (selection ? "openrouter" : "default"))
167
+ } else if (request.connectionId !== endpointModelSelectionSource(selection))
160
168
  return result("NOT_CONFIGURED");
161
169
  const apiKeyFile =
162
170
  env.SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND === "ADMIN"
@@ -183,10 +191,13 @@ export async function verifyRunsApiSelection(
183
191
  },
184
192
  })
185
193
  : makeDefaultClient({ apiKeyFile });
194
+ // A fresh token per check. Claude Opus 5.5 answers one fixed string
195
+ // ("SELLABLE_CONNECTION_OK") with nothing, so Claude Code fell back to
196
+ // another model and every Claude Code verification failed.
197
+ const token = `ready-${randomUUID().replaceAll("-", "").slice(0, 8)}`;
186
198
  const compiled = {
187
- input: "Connection verification. Reply with SELLABLE_CONNECTION_OK.",
188
- instructions:
189
- "This is a connection check. Do not use tools or take actions. Return exactly SELLABLE_CONNECTION_OK and nothing else.",
199
+ input: `Connection verification. Reply with ${token}.`,
200
+ instructions: `This is a connection check. Do not use tools or take actions. Return exactly ${token} and nothing else.`,
190
201
  conversationHistory: [],
191
202
  correlation: `connection-check-${randomUUID()}`,
192
203
  ...(selection ? { modelSelection: selection } : {}),
@@ -203,7 +214,7 @@ export async function verifyRunsApiSelection(
203
214
  if (
204
215
  status.status !== "completed" ||
205
216
  typeof status.output !== "string" ||
206
- status.output.trim() !== "SELLABLE_CONNECTION_OK" ||
217
+ status.output.trim() !== token ||
207
218
  !served
208
219
  )
209
220
  return result("UNAVAILABLE");