fix(runtime): pass provider args through codex probes

This commit is contained in:
777genius 2026-04-23 21:29:19 +03:00
parent 155a9f76ab
commit 0bde66b032
4 changed files with 165 additions and 23 deletions

View file

@ -44,7 +44,7 @@ interface ProviderModelAvailabilityCacheEntry {
providerId: CliProviderId; providerId: CliProviderId;
signature: string; signature: string;
snapshot: ProviderModelAvailabilitySnapshot; snapshot: ProviderModelAvailabilitySnapshot;
envPromise: Promise<NodeJS.ProcessEnv>; cliEnvPromise: Promise<{ env: NodeJS.ProcessEnv; providerArgs: string[] }>;
} }
type ProviderAvailabilityUpdateHandler = ( type ProviderAvailabilityUpdateHandler = (
@ -190,10 +190,13 @@ export class CliProviderModelAvailabilityService {
providerId: context.provider.providerId, providerId: context.provider.providerId,
signature, signature,
snapshot: createCheckingSnapshot(signature, visibleModels), snapshot: createCheckingSnapshot(signature, visibleModels),
envPromise: buildProviderAwareCliEnv({ cliEnvPromise: buildProviderAwareCliEnv({
binaryPath: context.binaryPath, binaryPath: context.binaryPath,
providerId: context.provider.providerId, providerId: context.provider.providerId,
}).then((result) => result.env), }).then((result) => ({
env: result.env,
providerArgs: result.providerArgs ?? [],
})),
}; };
this.cache.set(signature, entry); this.cache.set(signature, entry);
this.startProbes(context, entry); this.startProbes(context, entry);
@ -268,11 +271,15 @@ export class CliProviderModelAvailabilityService {
modelId: string modelId: string
): Promise<Pick<CliProviderModelAvailability, 'status' | 'reason'>> { ): Promise<Pick<CliProviderModelAvailability, 'status' | 'reason'>> {
try { try {
const env = await entry.envPromise; const { env, providerArgs } = await entry.cliEnvPromise;
const { stdout } = await execCli(context.binaryPath, buildProviderModelProbeArgs(modelId), { const { stdout } = await execCli(
timeout: getProviderModelProbeTimeoutMs(context.provider.providerId), context.binaryPath,
env, [...buildProviderModelProbeArgs(modelId), ...providerArgs],
}); {
timeout: getProviderModelProbeTimeoutMs(context.provider.providerId),
env,
}
);
const output = stdout.trim(); const output = stdout.trim();
if (isProviderModelProbeSuccessOutput(output)) { if (isProviderModelProbeSuccessOutput(output)) {
return { return {

View file

@ -3563,11 +3563,13 @@ export class TeamProvisioningService {
cwd: string; cwd: string;
providerId: TeamProviderId; providerId: TeamProviderId;
env: NodeJS.ProcessEnv; env: NodeJS.ProcessEnv;
providerArgs?: string[];
limitContext?: boolean; limitContext?: boolean;
}): Promise<RuntimeProviderLaunchFacts> { }): Promise<RuntimeProviderLaunchFacts> {
const providerArgs = params.providerArgs ?? [];
const modelListPromise = execCli( const modelListPromise = execCli(
params.claudePath, params.claudePath,
['model', 'list', '--json', '--provider', params.providerId], ['model', 'list', '--json', '--provider', params.providerId, ...providerArgs],
{ {
cwd: params.cwd, cwd: params.cwd,
env: params.env, env: params.env,
@ -3578,7 +3580,7 @@ export class TeamProvisioningService {
params.providerId === 'codex' || params.providerId === 'anthropic' params.providerId === 'codex' || params.providerId === 'anthropic'
? execCli( ? execCli(
params.claudePath, params.claudePath,
['runtime', 'status', '--json', '--provider', params.providerId], ['runtime', 'status', '--json', '--provider', params.providerId, ...providerArgs],
{ {
cwd: params.cwd, cwd: params.cwd,
env: params.env, env: params.env,
@ -7434,11 +7436,11 @@ export class TeamProvisioningService {
const isAuthFailure = this.isAuthFailureWarning(probeResult.warning, 'probe'); const isAuthFailure = this.isAuthFailureWarning(probeResult.warning, 'probe');
const isBlockingPreflightWarning = const isBlockingPreflightWarning =
authSource === 'configured_api_key_missing' || authSource === 'configured_api_key_missing' ||
(((authSource === 'none' || ((authSource === 'none' ||
authSource === 'codex_runtime' || authSource === 'codex_runtime' ||
authSource === 'gemini_runtime') && authSource === 'gemini_runtime') &&
isAuthFailure) || isAuthFailure) ||
isBinaryProbeWarning(probeResult.warning)); isBinaryProbeWarning(probeResult.warning);
if (authSource === 'configured_api_key_missing') { if (authSource === 'configured_api_key_missing') {
blockingMessages.push(prefixedWarning); blockingMessages.push(prefixedWarning);
} else if ( } else if (
@ -7849,12 +7851,13 @@ export class TeamProvisioningService {
return { details, warnings, blockingMessages }; return { details, warnings, blockingMessages };
} }
const { env } = await this.buildProvisioningEnv(providerId); const { env, providerArgs = [] } = await this.buildProvisioningEnv(providerId);
const runtimeFacts = await this.readRuntimeProviderLaunchFacts({ const runtimeFacts = await this.readRuntimeProviderLaunchFacts({
claudePath, claudePath,
cwd, cwd,
providerId, providerId,
env, env,
providerArgs,
limitContext, limitContext,
}); });
const probeOutcomeByResolvedModelId = new Map< const probeOutcomeByResolvedModelId = new Map<
@ -7912,6 +7915,7 @@ export class TeamProvisioningService {
cwd, cwd,
providerId, providerId,
env, env,
providerArgs,
limitContext limitContext
); );
} catch { } catch {
@ -7966,7 +7970,7 @@ export class TeamProvisioningService {
try { try {
const result = await this.spawnProbe( const result = await this.spawnProbe(
claudePath, claudePath,
buildProviderModelProbeArgs(targetModelId), [...buildProviderModelProbeArgs(targetModelId), ...providerArgs],
cwd, cwd,
env, env,
getProviderModelProbeTimeoutMs(providerId), getProviderModelProbeTimeoutMs(providerId),
@ -8069,13 +8073,18 @@ export class TeamProvisioningService {
cwd: string, cwd: string,
providerId: TeamProviderId, providerId: TeamProviderId,
env: NodeJS.ProcessEnv, env: NodeJS.ProcessEnv,
providerArgs: string[] = [],
limitContext: boolean limitContext: boolean
): Promise<string | null> { ): Promise<string | null> {
const { stdout } = await execCli(claudePath, ['model', 'list', '--json', '--provider', 'all'], { const { stdout } = await execCli(
cwd, claudePath,
env, ['model', 'list', '--json', '--provider', 'all', ...providerArgs],
timeout: 10_000, {
}); cwd,
env,
timeout: 10_000,
}
);
const parsed = extractJsonObjectFromCli<ProviderModelListCommandResponse>(stdout); const parsed = extractJsonObjectFromCli<ProviderModelListCommandResponse>(stdout);
const defaultModel = parsed.providers?.[providerId]?.defaultModel; const defaultModel = parsed.providers?.[providerId]?.defaultModel;
const normalizedDefaultModel = const normalizedDefaultModel =
@ -8145,6 +8154,7 @@ export class TeamProvisioningService {
params.cwd, params.cwd,
providerId, providerId,
envResolution.env, envResolution.env,
envResolution.providerArgs,
params.limitContext === true params.limitContext === true
); );
const normalized = resolvedDefaultModel?.trim(); const normalized = resolvedDefaultModel?.trim();
@ -8240,7 +8250,12 @@ export class TeamProvisioningService {
const claudePath = await ClaudeBinaryResolver.resolve(); const claudePath = await ClaudeBinaryResolver.resolve();
if (!claudePath) return null; if (!claudePath) return null;
const { env, authSource, warning } = await this.buildProvisioningEnv(providerId); const {
env,
authSource,
providerArgs = [],
warning,
} = await this.buildProvisioningEnv(providerId);
if (warning) { if (warning) {
return { return {
claudePath, claudePath,
@ -8249,7 +8264,7 @@ export class TeamProvisioningService {
}; };
} }
const probe = await this.probeClaudeRuntime(claudePath, cwd, env, providerId); const probe = await this.probeClaudeRuntime(claudePath, cwd, env, providerId, providerArgs);
const result = { const result = {
claudePath, claudePath,
authSource, authSource,
@ -18476,7 +18491,8 @@ export class TeamProvisioningService {
claudePath: string, claudePath: string,
cwd: string, cwd: string,
env: NodeJS.ProcessEnv, env: NodeJS.ProcessEnv,
providerId: TeamProviderId | undefined = 'anthropic' providerId: TeamProviderId | undefined = 'anthropic',
providerArgs: string[] = []
): Promise<{ warning?: string }> { ): Promise<{ warning?: string }> {
const resolvedProviderId = resolveTeamProviderId(providerId); const resolvedProviderId = resolveTeamProviderId(providerId);
const cliCommandLabel = getConfiguredCliCommandLabel(); const cliCommandLabel = getConfiguredCliCommandLabel();
@ -18543,7 +18559,7 @@ export class TeamProvisioningService {
try { try {
pingProbe = await this.spawnProbe( pingProbe = await this.spawnProbe(
claudePath, claudePath,
getPreflightPingArgs(providerId), [...getPreflightPingArgs(providerId), ...providerArgs],
cwd, cwd,
env, env,
getPreflightTimeoutMs(providerId), getPreflightTimeoutMs(providerId),

View file

@ -152,4 +152,29 @@ describe('CliProviderModelAvailabilityService', () => {
expect(execCliMock).toHaveBeenCalledTimes(3); expect(execCliMock).toHaveBeenCalledTimes(3);
}); });
}); });
it('passes provider launch args into codex model probes', async () => {
buildProviderAwareCliEnvMock.mockResolvedValue({
env: { HOME: '/Users/tester' },
providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
connectionIssues: {},
});
execCliMock.mockResolvedValue({ stdout: 'PONG', stderr: '' });
const service = new CliProviderModelAvailabilityService();
service.getSnapshot(createContext(['gpt-5.4']));
await vi.waitFor(() => {
expect(execCliMock).toHaveBeenCalledWith(
'/usr/local/bin/claude',
expect.arrayContaining([
'--settings',
'{"codex":{"forced_login_method":"chatgpt"}}',
]),
expect.objectContaining({
env: { HOME: '/Users/tester' },
})
);
});
});
}); });

View file

@ -1025,6 +1025,47 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
expect(result.warning).toContain('request id: req_123'); expect(result.warning).toContain('request id: req_123');
}); });
it('passes provider launch args into codex preflight ping probes', async () => {
const svc = new TeamProvisioningService();
const spawnProbe = vi
.spyOn(svc as any, 'spawnProbe')
.mockResolvedValueOnce({
stdout: 'orchestrator-cli 1.2.3',
stderr: '',
exitCode: 0,
})
.mockResolvedValueOnce({
stdout: 'PONG',
stderr: '',
exitCode: 0,
});
const result = await (svc as any).probeClaudeRuntime(
'/fake/claude',
tempRoot,
{
PATH: '/usr/bin',
SHELL: '/bin/zsh',
},
'codex',
['--settings', '{"codex":{"forced_login_method":"chatgpt"}}']
);
expect(result.warning).toBeUndefined();
expect(spawnProbe).toHaveBeenNthCalledWith(
2,
'/fake/claude',
expect.arrayContaining([
'--settings',
'{"codex":{"forced_login_method":"chatgpt"}}',
]),
tempRoot,
expect.any(Object),
60_000,
expect.any(Object)
);
});
it('continues selected model verification after transient preflight warnings', async () => { it('continues selected model verification after transient preflight warnings', async () => {
const svc = new TeamProvisioningService(); const svc = new TeamProvisioningService();
vi.spyOn(svc as any, 'getCachedOrProbeResult').mockResolvedValue({ vi.spyOn(svc as any, 'getCachedOrProbeResult').mockResolvedValue({
@ -1091,6 +1132,59 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
); );
}); });
it('passes provider launch args into selected codex model probes', async () => {
const svc = new TeamProvisioningService();
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
env: {
PATH: '/usr/bin',
SHELL: '/bin/zsh',
},
authSource: 'codex_runtime',
geminiRuntimeAuth: null,
providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
});
const readRuntimeProviderLaunchFacts = vi
.spyOn(svc as any, 'readRuntimeProviderLaunchFacts')
.mockResolvedValue({
defaultModel: null,
modelIds: new Set(['gpt-5.4']),
modelCatalog: null,
runtimeCapabilities: null,
providerStatus: null,
});
const spawnProbe = vi.spyOn(svc as any, 'spawnProbe').mockResolvedValue({
stdout: 'PONG',
stderr: '',
exitCode: 0,
});
const result = await (svc as any).verifySelectedProviderModels({
claudePath: '/fake/claude',
cwd: tempRoot,
providerId: 'codex',
modelIds: ['gpt-5.4'],
limitContext: false,
});
expect(result.details).toEqual(['Selected model gpt-5.4 verified for launch.']);
expect(readRuntimeProviderLaunchFacts).toHaveBeenCalledWith(
expect.objectContaining({
providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
})
);
expect(spawnProbe).toHaveBeenCalledWith(
'/fake/claude',
expect.arrayContaining([
'--settings',
'{"codex":{"forced_login_method":"chatgpt"}}',
]),
tempRoot,
expect.any(Object),
60_000,
expect.any(Object)
);
});
it('maps ANTHROPIC_AUTH_TOKEN into ANTHROPIC_API_KEY for headless preflight', async () => { it('maps ANTHROPIC_AUTH_TOKEN into ANTHROPIC_API_KEY for headless preflight', async () => {
const svc = new TeamProvisioningService(); const svc = new TeamProvisioningService();
vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({ vi.mocked(resolveInteractiveShellEnv).mockResolvedValue({