fix(runtime): pass provider args through codex probes
This commit is contained in:
parent
155a9f76ab
commit
0bde66b032
4 changed files with 165 additions and 23 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue