fix(opencode): keep model switching runtime-hot
This commit is contained in:
@@ -412,68 +412,76 @@ describe('provider host api routes', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('restarts a running runtime when the local proxy Host API token changed', async () => {
|
||||
const existingAccount = createProviderAccount({
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
label: 'Makelore Models',
|
||||
authMode: 'api_key',
|
||||
baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1',
|
||||
apiProtocol: 'openai-completions',
|
||||
model: 'gpt-4.1-mini',
|
||||
fallbackModels: ['gpt-4o-mini'],
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
metadata: {
|
||||
customModels: ['gpt-4.1-mini', 'gpt-4o-mini'],
|
||||
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
|
||||
worksSquareOneApiBaseUrl: 'https://one-api.example.com/v1',
|
||||
},
|
||||
});
|
||||
providerServiceMock.getAccount.mockResolvedValueOnce(existingAccount);
|
||||
providerServiceMock.getAccountApiKey.mockResolvedValueOnce('old-host-api-token');
|
||||
getHostApiTokenMock.mockReturnValueOnce('new-host-api-token');
|
||||
providerServiceMock.updateAccount.mockResolvedValueOnce(existingAccount);
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({
|
||||
provider_type: 'openai-compatible',
|
||||
it.each([
|
||||
['fresh', false],
|
||||
['attached', true],
|
||||
] as const)(
|
||||
'%s runtime handles a changed local proxy Host API token without losing freshness guarantees',
|
||||
async (provenance, runtimeRefreshRequired) => {
|
||||
const existingAccount = createProviderAccount({
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
label: 'Makelore Models',
|
||||
base_url: 'https://one-api.example.com/v1',
|
||||
api_key: 'fresh-ws-ai-token',
|
||||
credential_mode: 'works_square_ai_gateway',
|
||||
api_key_expires_in: 3600,
|
||||
models: ['gpt-4.1-mini', 'gpt-4o-mini'],
|
||||
}), { status: 200 }),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
const restart = vi.fn(async () => ({ state: 'running', port: 4096, pid: 4242 }));
|
||||
|
||||
await handleProviderRoutes(
|
||||
createRequest('POST', { accessToken: 'access-token' }),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/provider-accounts/import-user-model-config'),
|
||||
{
|
||||
opencodeManager: {
|
||||
getStatus: () => ({ state: 'running', port: 4096 }),
|
||||
restart,
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(providerServiceMock.updateAccount).toHaveBeenCalledWith(
|
||||
'niancode-user-models',
|
||||
expect.objectContaining({
|
||||
authMode: 'api_key',
|
||||
baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1',
|
||||
metadata: expect.objectContaining({
|
||||
apiProtocol: 'openai-completions',
|
||||
model: 'gpt-4.1-mini',
|
||||
fallbackModels: ['gpt-4o-mini'],
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
metadata: {
|
||||
customModels: ['gpt-4.1-mini', 'gpt-4o-mini'],
|
||||
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
|
||||
worksSquareOneApiBaseUrl: 'https://one-api.example.com/v1',
|
||||
},
|
||||
});
|
||||
providerServiceMock.getAccount.mockResolvedValueOnce(existingAccount);
|
||||
providerServiceMock.getAccountApiKey.mockResolvedValueOnce('old-host-api-token');
|
||||
getHostApiTokenMock.mockReturnValueOnce('new-host-api-token');
|
||||
providerServiceMock.updateAccount.mockResolvedValueOnce(existingAccount);
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({
|
||||
provider_type: 'openai-compatible',
|
||||
label: 'Makelore Models',
|
||||
base_url: 'https://one-api.example.com/v1',
|
||||
api_key: 'fresh-ws-ai-token',
|
||||
credential_mode: 'works_square_ai_gateway',
|
||||
api_key_expires_in: 3600,
|
||||
models: ['gpt-4.1-mini', 'gpt-4o-mini'],
|
||||
}), { status: 200 }),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const response = createResponse();
|
||||
const restart = vi.fn(async () => ({ state: 'running', port: 4096, pid: 4242 }));
|
||||
|
||||
await handleProviderRoutes(
|
||||
createRequest('POST', { accessToken: 'access-token' }),
|
||||
response.res,
|
||||
new URL('http://127.0.0.1/api/provider-accounts/import-user-model-config'),
|
||||
{
|
||||
opencodeManager: {
|
||||
getStatus: () => ({ state: 'running', port: 4096 }),
|
||||
getRuntimeGenerationProvenance: () => provenance,
|
||||
restart,
|
||||
},
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({ runtimeRefreshRequired });
|
||||
expect(providerServiceMock.updateAccount).toHaveBeenCalledWith(
|
||||
'niancode-user-models',
|
||||
expect.objectContaining({
|
||||
baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1',
|
||||
metadata: expect.objectContaining({
|
||||
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
'new-host-api-token',
|
||||
);
|
||||
expect(restart).toHaveBeenCalledOnce();
|
||||
});
|
||||
'new-host-api-token',
|
||||
);
|
||||
expect(restart).toHaveBeenCalledTimes(runtimeRefreshRequired ? 1 : 0);
|
||||
},
|
||||
);
|
||||
|
||||
it('defers a direct API-key rotation without restarting and reports the runtime refresh requirement', async () => {
|
||||
const existingAccount = createProviderAccount({
|
||||
|
||||
Reference in New Issue
Block a user