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:
Ray Xi
2026-07-21 10:17:14 +00:00
committed by GitHub
parent d1372da029
commit 068c9ae83f
3 changed files with 121 additions and 7 deletions
+16 -2
View File
@@ -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' },
+13 -5
View File
@@ -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 },