mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(daemon): normalize Anthropic-compatible BYOK base URLs (#5807)
* fix(daemon): normalize Anthropic-compatible BYOK base URLs * test(e2e): stabilize home plugin details flow Wait for Home project loading to settle before revealing templates, then target the semantic Details control after verifying actionability so layout shifts cannot collapse the gallery or trigger the wrong card action. Generated-By: looper 0.10.4 (runner=fixer, agent=codex) * fix(daemon): version Anthropic gateway URL paths Append /v1 through the parsed URL pathname so gateway query parameters and fragments remain in their proper URL components. Preserve the prior string-concatenation fallback for invalid URLs. Generated-By: looper 0.10.4 (runner=fixer, agent=codex)
This commit is contained in:
@@ -103,8 +103,8 @@ function normalizeProviderBaseUrl(
|
||||
): string {
|
||||
const trimmed = baseUrl.trim().replace(/\/+$/, '');
|
||||
if (!trimmed) return trimmed;
|
||||
if (protocol === 'anthropic' && isExactOrigin(trimmed, 'https://api.anthropic.com')) {
|
||||
return 'https://api.anthropic.com/v1';
|
||||
if (protocol === 'anthropic' && !hasVersionedApiPath(trimmed)) {
|
||||
return appendVersionedApiPath(trimmed);
|
||||
}
|
||||
if (protocol === 'openai' && isExactOrigin(trimmed, 'https://api.openai.com')) {
|
||||
return 'https://api.openai.com/v1';
|
||||
@@ -257,6 +257,20 @@ function safeUrlPathname(value: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function hasVersionedApiPath(value: string): boolean {
|
||||
return /\/v\d+(?:\/|$)/.test(safeUrlPathname(value));
|
||||
}
|
||||
|
||||
function appendVersionedApiPath(value: string): string {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
url.pathname = `${url.pathname.replace(/\/+$/, '')}/v1`;
|
||||
return url.toString();
|
||||
} catch {
|
||||
return `${value}/v1`;
|
||||
}
|
||||
}
|
||||
|
||||
function apiVersionOption(
|
||||
apiVersion: string | undefined,
|
||||
omitWhenBlank: boolean,
|
||||
|
||||
@@ -151,6 +151,98 @@ describe('byok-opencode runtime config', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'MiniMax',
|
||||
baseUrl: 'https://api.minimax.io/anthropic',
|
||||
expectedBaseUrl: 'https://api.minimax.io/anthropic/v1',
|
||||
},
|
||||
{
|
||||
name: 'DeepSeek',
|
||||
baseUrl: 'https://api.deepseek.com/anthropic',
|
||||
expectedBaseUrl: 'https://api.deepseek.com/anthropic/v1',
|
||||
},
|
||||
{
|
||||
name: 'MiMo',
|
||||
baseUrl: 'https://token-plan-cn.xiaomimimo.com/anthropic',
|
||||
expectedBaseUrl: 'https://token-plan-cn.xiaomimimo.com/anthropic/v1',
|
||||
},
|
||||
])(
|
||||
'adds the Anthropic API version to the $name compatibility base URL',
|
||||
({ baseUrl, expectedBaseUrl }) => {
|
||||
expect(buildOpenCodeByokProviderConfig(
|
||||
{
|
||||
protocol: 'anthropic',
|
||||
apiKey: 'anthropic-compatible-key',
|
||||
baseUrl,
|
||||
},
|
||||
'anthropic-compatible-model',
|
||||
)?.config).toMatchObject({
|
||||
provider: {
|
||||
[BYOK_OPENCODE_PROVIDER_ID]: {
|
||||
npm: '@ai-sdk/anthropic',
|
||||
options: {
|
||||
baseURL: expectedBaseUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('preserves an Anthropic compatibility base URL that already has a version segment', () => {
|
||||
expect(buildOpenCodeByokProviderConfig(
|
||||
{
|
||||
protocol: 'anthropic',
|
||||
apiKey: 'anthropic-compatible-key',
|
||||
baseUrl: 'https://gateway.example.com/anthropic/v2/proxy',
|
||||
},
|
||||
'anthropic-compatible-model',
|
||||
)?.config).toMatchObject({
|
||||
provider: {
|
||||
[BYOK_OPENCODE_PROVIDER_ID]: {
|
||||
npm: '@ai-sdk/anthropic',
|
||||
options: {
|
||||
baseURL: 'https://gateway.example.com/anthropic/v2/proxy',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'query parameters',
|
||||
baseUrl: 'https://gateway.example.com/anthropic?tenant=x',
|
||||
expectedBaseUrl: 'https://gateway.example.com/anthropic/v1?tenant=x',
|
||||
},
|
||||
{
|
||||
name: 'a fragment',
|
||||
baseUrl: 'https://gateway.example.com/anthropic#route',
|
||||
expectedBaseUrl: 'https://gateway.example.com/anthropic/v1#route',
|
||||
},
|
||||
])(
|
||||
'preserves $name when versioning an Anthropic compatibility base URL',
|
||||
({ baseUrl, expectedBaseUrl }) => {
|
||||
expect(buildOpenCodeByokProviderConfig(
|
||||
{
|
||||
protocol: 'anthropic',
|
||||
apiKey: 'anthropic-compatible-key',
|
||||
baseUrl,
|
||||
},
|
||||
'anthropic-compatible-model',
|
||||
)?.config).toMatchObject({
|
||||
provider: {
|
||||
[BYOK_OPENCODE_PROVIDER_ID]: {
|
||||
options: {
|
||||
baseURL: expectedBaseUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('maps other native BYOK protocols to provider packages', () => {
|
||||
expect(buildOpenCodeByokProviderConfig(
|
||||
{ protocol: 'anthropic', apiKey: 'sk-ant', baseUrl: 'https://api.anthropic.com' },
|
||||
|
||||
@@ -2429,8 +2429,16 @@ async function openHomePluginDetails(
|
||||
const home = scopedHome;
|
||||
const card = home.locator(`article.plugins-home__card[data-plugin-id="${pluginId}"]`).first();
|
||||
await expect(card).toBeVisible();
|
||||
await clickCardAtActionablePoint(page, card);
|
||||
const detailsButton = card.getByTestId(`plugins-home-details-${pluginId}`);
|
||||
await expect(detailsButton).toBeVisible();
|
||||
const dialog = page.getByRole('dialog').filter({ hasText: name });
|
||||
await scrollCardIntoActionableView(card);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return (await getActionablePoint(detailsButton).catch(() => null)) !== null;
|
||||
}, { timeout: T.medium })
|
||||
.toBe(true);
|
||||
await detailsButton.dispatchEvent('click');
|
||||
await expect(dialog).toBeVisible();
|
||||
return dialog;
|
||||
}
|
||||
@@ -2450,18 +2458,18 @@ async function clickCardAtActionablePoint(page: Page, card: Locator) {
|
||||
await scrollCardIntoActionableView(card);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return (await getCardActionablePoint(card)) !== null;
|
||||
return (await getActionablePoint(card)) !== null;
|
||||
}, { timeout: 5_000 })
|
||||
.toBe(true);
|
||||
const point = await getCardActionablePoint(card);
|
||||
const point = await getActionablePoint(card);
|
||||
if (!point) {
|
||||
throw new Error('Plugin card did not expose an actionable click point.');
|
||||
}
|
||||
await page.mouse.click(point.x, point.y);
|
||||
}
|
||||
|
||||
async function getCardActionablePoint(card: Locator) {
|
||||
return card.evaluate((element) => {
|
||||
async function getActionablePoint(target: Locator) {
|
||||
return target.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const points = [
|
||||
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
||||
|
||||
Reference in New Issue
Block a user