Skip to content

Commit 2b19511

Browse files
mrcfpsopen-design-bot[bot]
authored andcommitted
fix(amr): improve unavailable model gating (nexu-io#5335)
* fix(amr): honor full model list metadata * fix(amr): classify tier entitlement failures * fix(amr): refine unavailable model upgrade affordance * fix(web): restore stale AMR model fallback Generated-By: looper 0.10.1 (runner=fixer, agent=codex) * fix(web): surface AMR tier upgrade failures Generated-By: looper 0.10.1 (runner=fixer, agent=codex) * fix(amr): mirror chat upgrade attribution Generated-By: looper 0.10.1 (runner=fixer, agent=codex) * fix(amr): skip disabled default models Generated-By: looper 0.10.1 (runner=fixer, agent=codex) * fix(amr): honor catalog default model * chore(pack): bump vela cli to 0.0.20 * fix(amr): align locked catalog fallback state * chore(nix): refresh pnpm deps hash * fix(amr): refresh entitlement-aware model state * fix(amr): refresh models after upgrade return * fix(amr): preserve default model analytics * fix(amr): tighten model gating follow-ups Generated-By: looper 0.10.1 (runner=fixer, agent=codex) * fix(amr): repair default and refresh follow-ups Generated-By: looper 0.10.1 (runner=fixer, agent=codex) * fix(amr): resolve explicit default before spawn Generated-By: looper 0.10.1 (runner=fixer, agent=codex) * fix(amr): concretize default model in smoke paths Generated-By: looper 0.10.1 (runner=fixer, agent=codex) * fix(amr): repair default model gating Generated-By: looper 0.10.1 (runner=fixer, agent=codex) * fix(amr): share connection-test model cache Generated-By: looper 0.10.1 (runner=fixer, agent=codex) --------- Co-authored-by: open-design-bot[bot] <282769551+open-design-bot[bot]@users.noreply.github.com>
1 parent adfd3f6 commit 2b19511

75 files changed

Lines changed: 1750 additions & 213 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/daemon/src/connectionTest.ts

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import {
5656
import { aihubmixHeaders } from './integrations/aihubmix.js';
5757
import type { AgentCliEnvPrefs } from './app-config.js';
5858
import type { RuntimeAgentDef } from './runtimes/types.js';
59-
import { resolveModelForAgent } from './runtimes/models.js';
6059
import { preparePromptFileForAgent, type PreparedPromptFile } from './runtimes/prompt-file.js';
6160
import { configuredAllowedInternalHosts } from './origin-validation.js';
6261
import {
@@ -76,7 +75,19 @@ import {
7675
type ProviderTestRequest,
7776
} from '@open-design/contracts/api/connectionTest';
7877
import { googleGenerateContentUrl } from './integrations/google-models.js';
79-
import { resolveAmrProfile } from './integrations/vela.js';
78+
import { readVelaCredentialRevision, resolveAmrProfile } from './integrations/vela.js';
79+
import { amrModelLoadingCache } from './runtimes/amr-model-cache.js';
80+
import { buildAmrModelCacheKey } from './runtimes/amr-model-probe.js';
81+
import {
82+
fetchVelaPresetModels,
83+
fetchVelaRemoteModelsWithRetry,
84+
} from './runtimes/defs/amr.js';
85+
import {
86+
getRememberedLiveModels,
87+
preferFreshLiveModels,
88+
resolveDefaultModelFromOptions,
89+
resolveModelForAgent,
90+
} from './runtimes/models.js';
8091

8192
export { validateBaseUrl } from '@open-design/contracts/api/connectionTest';
8293

@@ -1880,12 +1891,9 @@ function attachAgentStreamHandlers(
18801891
child,
18811892
prompt,
18821893
cwd,
1883-
// Same substitution as the chat-run path in server.ts — adapters whose
1884-
// CLI rejects the synthetic 'default' (e.g. AMR / vela, which forces
1885-
// session/set_model before session/prompt) need the def's first
1886-
// concrete fallback id here too, otherwise Test connection deadlocks
1887-
// on the same `session/set_model must be called before session/prompt`
1888-
// error the chat-run path already handles.
1894+
// Same substitution as the chat-run path in server.ts: omitted models can
1895+
// resolve to a concrete fallback, while an explicit 'default' is preserved
1896+
// so ACP runtimes can use their upstream configured default.
18891897
model: resolveModelForAgent(def as never, model ?? null, modelEnv, liveModelScope),
18901898
mcpServers: [],
18911899
send,
@@ -1963,11 +1971,41 @@ async function prepareOpenCodeConnectionTestCwd(tempDir: string): Promise<void>
19631971
}
19641972
}
19651973

1974+
async function resolveConnectionTestModelForAgent(
1975+
def: RuntimeAgentDef,
1976+
requestedModel: string | null,
1977+
env: NodeJS.ProcessEnv,
1978+
liveModelScope: string | null,
1979+
launchPath?: string | null,
1980+
): Promise<string | null> {
1981+
const resolved = resolveModelForAgent(def, requestedModel, env, liveModelScope);
1982+
if (def.id !== 'amr' || resolved !== 'default' || !launchPath) return resolved;
1983+
1984+
try {
1985+
const cacheKey = buildAmrModelCacheKey({
1986+
launchPath,
1987+
env,
1988+
credentialRevision: readVelaCredentialRevision(env),
1989+
});
1990+
const catalog = await amrModelLoadingCache.get(cacheKey, {
1991+
fetchPreset: () => fetchVelaPresetModels(launchPath, env),
1992+
fetchRemote: () => fetchVelaRemoteModelsWithRetry(launchPath, env),
1993+
});
1994+
const liveModels = preferFreshLiveModels(
1995+
catalog.models ?? [],
1996+
getRememberedLiveModels(def.id, liveModelScope),
1997+
);
1998+
return resolveDefaultModelFromOptions(liveModels) ?? resolved;
1999+
} catch {
2000+
return resolved;
2001+
}
2002+
}
2003+
19662004
async function testAgentConnectionInternal(
19672005
input: AgentConnectionInput,
19682006
): Promise<ConnectionTestResponse> {
19692007
const start = Date.now();
1970-
const model =
2008+
let model =
19712009
typeof input.model === 'string' && input.model.trim()
19722010
? input.model.trim()
19732011
: 'default';
@@ -2221,6 +2259,13 @@ async function testAgentConnectionInternal(
22212259
...baseEnv,
22222260
...(mmdRouteLaunchEnv || {}),
22232261
}, executableResolution);
2262+
model = await resolveConnectionTestModelForAgent(
2263+
def,
2264+
model,
2265+
env,
2266+
liveModelScope,
2267+
executableResolution.launchPath,
2268+
) ?? model;
22242269
const auth = await probeAgentAuthStatus(def, executableResolution.launchPath, env);
22252270
if (auth?.status === 'missing') {
22262271
// Preflight auth probe runs after binary resolution but before the
@@ -2278,7 +2323,7 @@ async function testAgentConnectionInternal(
22782323
child,
22792324
SMOKE_PROMPT,
22802325
tempDir,
2281-
input.model,
2326+
model,
22822327
env,
22832328
liveModelScope,
22842329
sink.send,

apps/daemon/src/integrations/vela-errors.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
export type AmrAccountErrorCode = 'AMR_AUTH_REQUIRED' | 'AMR_INSUFFICIENT_BALANCE';
1+
export type AmrAccountErrorCode =
2+
| 'AMR_AUTH_REQUIRED'
3+
| 'AMR_INSUFFICIENT_BALANCE'
4+
| 'AMR_TIER_UPGRADE_REQUIRED';
25

36
export interface AmrAccountFailure {
47
code: AmrAccountErrorCode;
58
message: string;
6-
action: 'relogin' | 'recharge';
9+
action: 'relogin' | 'recharge' | 'upgrade';
710
actionUrl?: string;
811
}
912

@@ -27,6 +30,12 @@ const AMR_AUTH_REQUIRED_MESSAGE =
2730
const AMR_INSUFFICIENT_BALANCE_MESSAGE =
2831
`AMR Cloud reported insufficient balance for this model. Recharge your AMR wallet at ${DEFAULT_AMR_RECHARGE_URL}, then retry this run.`;
2932

33+
const AMR_TIER_UPGRADE_REQUIRED_MESSAGE =
34+
'Your current AMR plan does not include this model or request type. Upgrade your AMR plan, or switch to an available model and retry.';
35+
36+
const AMR_TIER_REQUEST_KIND_NOT_ENTITLED_MESSAGE =
37+
'Your current AMR plan does not include this request type yet. Upgrade your AMR plan, or switch to a supported model and retry.';
38+
3039
function normalizeFailureText(text: string): string {
3140
return String(text || '').toLowerCase();
3241
}
@@ -74,6 +83,22 @@ export function classifyAmrAccountFailureDetails(details: unknown): AmrAccountFa
7483
};
7584
}
7685

86+
if (code === 'tier_model_not_entitled') {
87+
return {
88+
code: 'AMR_TIER_UPGRADE_REQUIRED',
89+
message: AMR_TIER_UPGRADE_REQUIRED_MESSAGE,
90+
action: 'upgrade',
91+
};
92+
}
93+
94+
if (code === 'tier_request_kind_not_entitled') {
95+
return {
96+
code: 'AMR_TIER_UPGRADE_REQUIRED',
97+
message: AMR_TIER_REQUEST_KIND_NOT_ENTITLED_MESSAGE,
98+
action: 'upgrade',
99+
};
100+
}
101+
77102
return null;
78103
}
79104

@@ -114,6 +139,22 @@ export function classifyAmrAccountFailure(text: string): AmrAccountFailure | nul
114139
};
115140
}
116141

142+
if (value.includes('tier_model_not_entitled')) {
143+
return {
144+
code: 'AMR_TIER_UPGRADE_REQUIRED',
145+
message: AMR_TIER_UPGRADE_REQUIRED_MESSAGE,
146+
action: 'upgrade',
147+
};
148+
}
149+
150+
if (value.includes('tier_request_kind_not_entitled')) {
151+
return {
152+
code: 'AMR_TIER_UPGRADE_REQUIRED',
153+
message: AMR_TIER_REQUEST_KIND_NOT_ENTITLED_MESSAGE,
154+
action: 'upgrade',
155+
};
156+
}
157+
117158
if (
118159
value.includes('auth_required') ||
119160
value.includes('authentication required') ||

apps/daemon/src/integrations/vela.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const AMR_ENTRY_SOURCES: ReadonlySet<TrackingAmrEntrySource> = new Set([
3030
'handoff_amr_website',
3131
'chat_error_authorize_retry',
3232
'chat_error_recharge',
33+
'chat_error_upgrade',
3334
'chat_balance_gate_upgrade',
3435
'home_balance_gate_upgrade',
3536
'chat_low_balance_warn_recharge',
@@ -80,6 +81,7 @@ const AMR_ENTRY_SOURCE_PAGE_BY_SOURCE: Record<
8081
handoff_amr_website: 'artifact',
8182
chat_error_authorize_retry: 'chat_panel',
8283
chat_error_recharge: 'chat_panel',
84+
chat_error_upgrade: 'chat_panel',
8385
chat_balance_gate_upgrade: 'chat_panel',
8486
home_balance_gate_upgrade: 'home',
8587
chat_low_balance_warn_recharge: 'chat_panel',

apps/daemon/src/routes/vela.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
velaWalletSnapshotReader,
3636
} from '../integrations/vela-wallet.js';
3737
import { amrModelLoadingCache } from '../runtimes/amr-model-cache.js';
38+
import { buildAmrModelCacheKey } from '../runtimes/amr-model-probe.js';
3839
import {
3940
fetchVelaBillingSummary,
4041
fetchVelaPresetModels,
@@ -169,14 +170,9 @@ export function registerVelaRoutes(app: Express, deps: RegisterVelaRoutesDeps):
169170
agentLaunch,
170171
);
171172
const credentialRevision = readVelaCredentialRevision(env, configuredEnv);
172-
const cacheKey = JSON.stringify({
173+
const cacheKey = buildAmrModelCacheKey({
173174
launchPath,
174-
home: spawnEnv.HOME ?? spawnEnv.USERPROFILE ?? '',
175-
openDesignAmrProfile: spawnEnv.OPEN_DESIGN_AMR_PROFILE ?? '',
176-
velaProfile: spawnEnv.VELA_PROFILE ?? '',
177-
velaLinkUrl: spawnEnv.VELA_LINK_URL ?? '',
178-
velaRuntimeKey: spawnEnv.VELA_RUNTIME_KEY ?? '',
179-
velaOpencodeBin: spawnEnv.VELA_OPENCODE_BIN ?? '',
175+
env: spawnEnv,
180176
credentialRevision,
181177
});
182178
return { launchPath, env: spawnEnv, configuredEnv, cacheKey };
@@ -197,17 +193,29 @@ export function registerVelaRoutes(app: Express, deps: RegisterVelaRoutesDeps):
197193
string,
198194
Promise<VelaLiveAccount | null>
199195
>();
196+
const inFlightVelaAccountInvalidations = new Set<string>();
200197
function fetchVelaLiveAccountSingleFlight(
201198
accountCacheKey: string,
202199
probe: AmrModelProbe,
200+
options: { invalidateModelsOnPlanChange?: boolean } = {},
203201
): Promise<VelaLiveAccount | null> {
202+
if (options.invalidateModelsOnPlanChange === true) {
203+
inFlightVelaAccountInvalidations.add(accountCacheKey);
204+
}
204205
const existing = inFlightVelaAccountFetches.get(accountCacheKey);
205206
if (existing) return existing;
206207
const pending = (async () => {
208+
const previousAccount = peekVelaLiveAccount(accountCacheKey);
207209
amrModelLoadingCache.warm(probe.cacheKey, () =>
208210
fetchVelaRemoteModelsWithRetry(probe.launchPath, probe.env),
209211
);
210212
const account = await fetchVelaBillingSummary(probe.launchPath, probe.env);
213+
if (
214+
inFlightVelaAccountInvalidations.has(accountCacheKey) &&
215+
(!previousAccount || previousAccount.plan !== account.plan)
216+
) {
217+
amrModelLoadingCache.invalidate(probe.cacheKey);
218+
}
211219
setVelaLiveAccount(accountCacheKey, account);
212220
return account;
213221
})()
@@ -220,6 +228,7 @@ export function registerVelaRoutes(app: Express, deps: RegisterVelaRoutesDeps):
220228
})
221229
.finally(() => {
222230
inFlightVelaAccountFetches.delete(accountCacheKey);
231+
inFlightVelaAccountInvalidations.delete(accountCacheKey);
223232
});
224233
inFlightVelaAccountFetches.set(accountCacheKey, pending);
225234
return pending;
@@ -242,6 +251,7 @@ export function registerVelaRoutes(app: Express, deps: RegisterVelaRoutesDeps):
242251
try {
243252
const appConfig = await readAppConfig(RUNTIME_DATA_DIR);
244253
const configuredEnv = agentCliEnvForAgent(appConfig.agentCliEnv, 'amr');
254+
const refresh = _req.query.refresh === '1' || _req.query.refresh === 'true';
245255
const status = readVelaLoginStatus(mergeVelaEnv(env, configuredEnv));
246256
if (status.loggedIn) {
247257
// Key the live-account cache by the full credential revision (not just
@@ -254,7 +264,12 @@ export function registerVelaRoutes(app: Express, deps: RegisterVelaRoutesDeps):
254264
);
255265
const probe = resolveAmrModelProbeForEnv(configuredEnv);
256266
const cachedAccount = peekVelaLiveAccount(accountCacheKey);
257-
if (!cachedAccount) {
267+
if (refresh) {
268+
const liveAccount = await fetchVelaLiveAccountSingleFlight(accountCacheKey, probe, {
269+
invalidateModelsOnPlanChange: true,
270+
});
271+
applyVelaLiveAccount(status, liveAccount);
272+
} else if (!cachedAccount) {
258273
// Cold cache (or a fetch already in flight): BLOCK on the single-flight
259274
// billing fetch so the first open already carries plan/balance. The
260275
// consumers (settings card, inline switcher, avatar) read /status once
@@ -274,7 +289,9 @@ export function registerVelaRoutes(app: Express, deps: RegisterVelaRoutesDeps):
274289
// next poll once the TTL has lapsed.
275290
applyVelaLiveAccount(status, cachedAccount);
276291
if (shouldRefreshVelaLiveAccount(accountCacheKey)) {
277-
void fetchVelaLiveAccountSingleFlight(accountCacheKey, probe).catch(() => {});
292+
void fetchVelaLiveAccountSingleFlight(accountCacheKey, probe, {
293+
invalidateModelsOnPlanChange: true,
294+
}).catch(() => {});
278295
}
279296
}
280297
}
@@ -294,6 +311,14 @@ export function registerVelaRoutes(app: Express, deps: RegisterVelaRoutesDeps):
294311
configuredEnv,
295312
refresh,
296313
});
314+
if (refresh) {
315+
try {
316+
const modelProbe = resolveAmrModelProbeForEnv(configuredEnv);
317+
amrModelLoadingCache.invalidate(modelProbe.cacheKey);
318+
} catch (err) {
319+
console.warn('[amr] model cache invalidation after wallet refresh failed', err);
320+
}
321+
}
297322
res.json(snapshot);
298323
} catch (err) {
299324
res.status(500).json({ error: String(err) });

apps/daemon/src/run-failure-classification.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,19 @@ export function classifyRunFailure(
549549
);
550550
}
551551

552+
if (
553+
errorCode === 'AMR_TIER_UPGRADE_REQUIRED' ||
554+
amrFailure?.code === 'AMR_TIER_UPGRADE_REQUIRED'
555+
) {
556+
return classification(
557+
'entitlement_required',
558+
'amr_tier_upgrade_required',
559+
'session_init',
560+
false,
561+
'upgrade',
562+
);
563+
}
564+
552565
if (
553566
errorCode === 'AMR_AUTH_REQUIRED' ||
554567
errorCode === 'AGENT_AUTH_REQUIRED' ||

apps/daemon/src/runtimes/amr-model-cache.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export class AmrModelLoadingCache {
6464
this.startRefresh(this.stateFor(cacheKey), fetchRemote);
6565
}
6666

67+
invalidate(cacheKey: string): void {
68+
this.states.delete(cacheKey);
69+
}
70+
6771
resetForTests(): void {
6872
this.states.clear();
6973
}

apps/daemon/src/runtimes/amr-model-probe.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,37 @@ import {
66
} from '../agents.js';
77
import { agentCliEnvForAgent, type readAppConfig } from '../app-config.js';
88
import { readVelaCredentialRevision } from '../integrations/vela.js';
9+
import type { VelaCredentialRevision } from '../integrations/vela.js';
910

1011
export interface ResolveAmrModelProbeDeps {
1112
dataDir: string;
1213
env: NodeJS.ProcessEnv;
1314
readAppConfig: typeof readAppConfig;
1415
}
1516

17+
export interface BuildAmrModelCacheKeyInput {
18+
launchPath: string;
19+
env: NodeJS.ProcessEnv;
20+
credentialRevision: VelaCredentialRevision;
21+
}
22+
23+
export function buildAmrModelCacheKey({
24+
launchPath,
25+
env,
26+
credentialRevision,
27+
}: BuildAmrModelCacheKeyInput): string {
28+
return JSON.stringify({
29+
launchPath,
30+
home: env.HOME ?? env.USERPROFILE ?? '',
31+
openDesignAmrProfile: env.OPEN_DESIGN_AMR_PROFILE ?? '',
32+
velaProfile: env.VELA_PROFILE ?? '',
33+
velaLinkUrl: env.VELA_LINK_URL ?? '',
34+
velaRuntimeKey: env.VELA_RUNTIME_KEY ?? '',
35+
velaOpencodeBin: env.VELA_OPENCODE_BIN ?? '',
36+
credentialRevision,
37+
});
38+
}
39+
1640
export async function resolveAmrModelProbe({
1741
dataDir,
1842
env: baseEnv,
@@ -38,14 +62,9 @@ export async function resolveAmrModelProbe({
3862
agentLaunch,
3963
);
4064
const credentialRevision = readVelaCredentialRevision(baseEnv, configuredEnv);
41-
const cacheKey = JSON.stringify({
65+
const cacheKey = buildAmrModelCacheKey({
4266
launchPath,
43-
home: env.HOME ?? env.USERPROFILE ?? '',
44-
openDesignAmrProfile: env.OPEN_DESIGN_AMR_PROFILE ?? '',
45-
velaProfile: env.VELA_PROFILE ?? '',
46-
velaLinkUrl: env.VELA_LINK_URL ?? '',
47-
velaRuntimeKey: env.VELA_RUNTIME_KEY ?? '',
48-
velaOpencodeBin: env.VELA_OPENCODE_BIN ?? '',
67+
env,
4968
credentialRevision,
5069
});
5170
return { launchPath, env, configuredEnv, cacheKey };

0 commit comments

Comments
 (0)