fix: isolate concurrent OpenCode chat runs
This commit is contained in:
1 parent
7e8d9e3811
commit
65040730ec
29 files changed
+5207
-427
No files matched your search
@@ -22,6 +22,10 @@ import {
|
||||
NIANCODE_USER_MODEL_ACCOUNT_LABEL,
|
||||
normalizeImportedUserModelId,
|
||||
} from '../../../shared/user-model-config';
|
||||
import {
|
||||
withRuntimeAcceptanceTimeout,
|
||||
withRuntimeConfigCoordinator,
|
||||
} from '../../opencode/runtime-config-readiness';
|
||||
|
||||
const legacyProviderRoutesWarned = new Set<string>();
|
||||
|
||||
@@ -238,13 +242,22 @@ function localAiProxyBaseUrl(): string {
|
||||
}
|
||||
|
||||
async function fetchCurrentUserModelConfig(accessToken: string): Promise<ImportedUserModelConfig> {
|
||||
const response = await proxyAwareFetch(createWorksUrl('/api/auth/me/model-config').toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
const { response, payload } = await withRuntimeAcceptanceTimeout(async (signal) => {
|
||||
const response = await proxyAwareFetch(
|
||||
createWorksUrl('/api/auth/me/model-config').toString(),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
signal,
|
||||
},
|
||||
);
|
||||
signal.throwIfAborted();
|
||||
const payload = await readResponsePayload(response);
|
||||
signal.throwIfAborted();
|
||||
return { response, payload };
|
||||
});
|
||||
const payload = await readResponsePayload(response);
|
||||
if (!response.ok) {
|
||||
throw new WorksSquareModelConfigError(
|
||||
response.status >= 400 && response.status < 500 ? response.status : 502,
|
||||
@@ -310,60 +323,98 @@ async function importedProviderApiKeyChanged(
|
||||
export async function importCurrentUserModelConfig(
|
||||
ctx: HostApiContext,
|
||||
accessToken: string,
|
||||
): Promise<{ account: ProviderAccount; importedModels: string[] }> {
|
||||
options: { runtimeRefresh?: 'apply' | 'defer' } = {},
|
||||
): Promise<{
|
||||
account: ProviderAccount;
|
||||
importedModels: string[];
|
||||
runtimeRefreshRequired: boolean;
|
||||
}> {
|
||||
const providerService = getProviderService();
|
||||
const modelConfig = await fetchCurrentUserModelConfig(accessToken);
|
||||
const useLocalAiProxy = modelConfig.credentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE;
|
||||
if (useLocalAiProxy) {
|
||||
seedWorksSquareAIGatewayCredential({
|
||||
accessToken: modelConfig.apiKey,
|
||||
expiresIn: modelConfig.apiKeyExpiresIn,
|
||||
oneApiBaseUrl: modelConfig.baseUrl,
|
||||
});
|
||||
}
|
||||
const nowMs = Date.now();
|
||||
const now = new Date(nowMs).toISOString();
|
||||
const existing = await providerService.getAccount(NIANCODE_USER_MODEL_ACCOUNT_ID);
|
||||
const accountBaseUrl = useLocalAiProxy ? localAiProxyBaseUrl() : modelConfig.baseUrl;
|
||||
const accountApiKey = useLocalAiProxy ? getHostApiToken() : modelConfig.apiKey;
|
||||
const orderedModels = orderImportedModelsForAccount(existing, modelConfig.models);
|
||||
const account: ProviderAccount = {
|
||||
id: NIANCODE_USER_MODEL_ACCOUNT_ID,
|
||||
vendorId: 'custom',
|
||||
label: modelConfig.label,
|
||||
authMode: 'api_key',
|
||||
baseUrl: accountBaseUrl,
|
||||
apiProtocol: 'openai-completions',
|
||||
headers: useLocalAiProxy ? undefined : importedUserModelHeaders(modelConfig),
|
||||
model: orderedModels[0],
|
||||
fallbackModels: orderedModels.slice(1),
|
||||
fallbackAccountIds: existing?.fallbackAccountIds,
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
metadata: importedUserModelMetadata(existing, modelConfig, nowMs, useLocalAiProxy),
|
||||
createdAt: existing?.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
};
|
||||
const shouldRestartRuntime = importedProviderRuntimeShapeChanged(existing, account)
|
||||
|| (useLocalAiProxy && await importedProviderApiKeyChanged(providerService, existing, accountApiKey));
|
||||
return await withRuntimeConfigCoordinator(ctx.opencodeManager, async (lease) => {
|
||||
let operationMarkedPending = false;
|
||||
try {
|
||||
return await withRuntimeAcceptanceTimeout(async (signal) => {
|
||||
signal.throwIfAborted();
|
||||
const useLocalAiProxy = modelConfig.credentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE;
|
||||
const nowMs = Date.now();
|
||||
const now = new Date(nowMs).toISOString();
|
||||
const existing = await providerService.getAccount(NIANCODE_USER_MODEL_ACCOUNT_ID);
|
||||
signal.throwIfAborted();
|
||||
const accountBaseUrl = useLocalAiProxy ? localAiProxyBaseUrl() : modelConfig.baseUrl;
|
||||
const accountApiKey = useLocalAiProxy ? getHostApiToken() : modelConfig.apiKey;
|
||||
const orderedModels = orderImportedModelsForAccount(existing, modelConfig.models);
|
||||
const account: ProviderAccount = {
|
||||
id: NIANCODE_USER_MODEL_ACCOUNT_ID,
|
||||
vendorId: 'custom',
|
||||
label: modelConfig.label,
|
||||
authMode: 'api_key',
|
||||
baseUrl: accountBaseUrl,
|
||||
apiProtocol: 'openai-completions',
|
||||
headers: useLocalAiProxy ? undefined : importedUserModelHeaders(modelConfig),
|
||||
model: orderedModels[0],
|
||||
fallbackModels: orderedModels.slice(1),
|
||||
fallbackAccountIds: existing?.fallbackAccountIds,
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
metadata: importedUserModelMetadata(existing, modelConfig, nowMs, useLocalAiProxy),
|
||||
createdAt: existing?.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
};
|
||||
const shouldRestartRuntime = importedProviderRuntimeShapeChanged(existing, account)
|
||||
|| await importedProviderApiKeyChanged(providerService, existing, accountApiKey);
|
||||
signal.throwIfAborted();
|
||||
const runtimeIsActive = ctx.opencodeManager.getStatus().state !== 'stopped';
|
||||
const refreshAlreadyPending = lease.isRefreshPending();
|
||||
const runtimeRefreshRequired = runtimeIsActive
|
||||
&& (shouldRestartRuntime || refreshAlreadyPending);
|
||||
const armStoppedApplyForNextFresh = !runtimeIsActive
|
||||
&& refreshAlreadyPending
|
||||
&& options.runtimeRefresh !== 'defer';
|
||||
if (runtimeRefreshRequired) {
|
||||
lease.markRefreshPending();
|
||||
operationMarkedPending = true;
|
||||
}
|
||||
|
||||
const savedAccount = existing
|
||||
? await providerService.updateAccount(
|
||||
NIANCODE_USER_MODEL_ACCOUNT_ID,
|
||||
account,
|
||||
accountApiKey,
|
||||
)
|
||||
: await providerService.createAccount(account, accountApiKey);
|
||||
signal.throwIfAborted();
|
||||
if (useLocalAiProxy) {
|
||||
seedWorksSquareAIGatewayCredential({
|
||||
accessToken: modelConfig.apiKey,
|
||||
expiresIn: modelConfig.apiKeyExpiresIn,
|
||||
oneApiBaseUrl: modelConfig.baseUrl,
|
||||
});
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
const savedAccount = existing
|
||||
? await providerService.updateAccount(
|
||||
NIANCODE_USER_MODEL_ACCOUNT_ID,
|
||||
account,
|
||||
accountApiKey,
|
||||
)
|
||||
: await providerService.createAccount(account, accountApiKey);
|
||||
signal.throwIfAborted();
|
||||
|
||||
await providerService.setDefaultAccount(NIANCODE_USER_MODEL_ACCOUNT_ID);
|
||||
if (shouldRestartRuntime) {
|
||||
await refreshRunningRuntimeAfterProviderChange(ctx);
|
||||
}
|
||||
await providerService.setDefaultAccount(NIANCODE_USER_MODEL_ACCOUNT_ID);
|
||||
signal.throwIfAborted();
|
||||
if (armStoppedApplyForNextFresh) {
|
||||
lease.markRefreshPending();
|
||||
}
|
||||
if (runtimeRefreshRequired && options.runtimeRefresh !== 'defer') {
|
||||
await refreshRunningRuntimeAfterProviderChange(ctx);
|
||||
signal.throwIfAborted();
|
||||
}
|
||||
|
||||
return {
|
||||
account: savedAccount,
|
||||
importedModels: modelConfig.models,
|
||||
};
|
||||
return {
|
||||
account: savedAccount,
|
||||
importedModels: modelConfig.models,
|
||||
runtimeRefreshRequired,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (operationMarkedPending) lease.retainRefreshPending();
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleProviderRoutes(
|
||||
@@ -467,11 +518,19 @@ export async function handleProviderRoutes(
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/provider-accounts/import-user-model-config' && req.method === 'POST') {
|
||||
let runtimeRefresh: 'apply' | 'defer' = 'apply';
|
||||
try {
|
||||
const body = await parseJsonBody<{ accessToken?: unknown }>(req);
|
||||
const body = await parseJsonBody<{ accessToken?: unknown; runtimeRefresh?: unknown }>(req);
|
||||
if (body.runtimeRefresh !== undefined) {
|
||||
if (body.runtimeRefresh !== 'apply' && body.runtimeRefresh !== 'defer') {
|
||||
throw new Error('Invalid runtimeRefresh mode');
|
||||
}
|
||||
runtimeRefresh = body.runtimeRefresh;
|
||||
}
|
||||
const result = await importCurrentUserModelConfig(
|
||||
ctx,
|
||||
readRequiredString(body.accessToken, 'accessToken'),
|
||||
{ runtimeRefresh },
|
||||
);
|
||||
sendJson(res, 200, { success: true, ...result });
|
||||
} catch (error) {
|
||||
|
||||
Reference in new issue
Block a user