Skip to content

Commit 94de08d

Browse files
authored
fix(ai-wandb): require clean api base urls (#822)
1 parent dc717a9 commit 94de08d

2 files changed

Lines changed: 46 additions & 4 deletions

File tree

packages/ai/wandb/src/index.test.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,26 +85,51 @@ describe('W&B Inference generation', () => {
8585
});
8686

8787
it('supports text-style choices from compatible responses', async () => {
88-
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
88+
const fetchMock = vi.fn().mockResolvedValue({
8989
ok: true,
9090
json: async () => ({
9191
choices: [{ text: 'legacy text response' }],
9292
}),
93-
}));
93+
});
94+
vi.stubGlobal('fetch', fetchMock);
9495

9596
const result = await adapter.generate(
9697
ctx(),
9798
'hello',
9899
{ model: 'deepseek-ai/DeepSeek-V3-0324' },
99-
{ baseUrl: 'https://wandb.test/v1' },
100+
{ baseUrl: 'https://wandb.test/v1/' },
100101
);
101102

103+
expect(fetchMock.mock.calls[0]?.[0]).toBe('https://wandb.test/v1/chat/completions');
102104
expect(result).toEqual({
103105
text: 'legacy text response',
104106
model: 'deepseek-ai/DeepSeek-V3-0324',
105107
});
106108
});
107109

110+
it('rejects invalid or unclean custom base URLs before network calls', async () => {
111+
const fetchMock = vi.fn();
112+
vi.stubGlobal('fetch', fetchMock);
113+
114+
await expect(
115+
adapter.generate(ctx(), 'hello', {}, { baseUrl: 'wandb.test/v1' }),
116+
).rejects.toThrow('valid URL');
117+
await expect(
118+
adapter.generate(ctx(), 'hello', {}, { baseUrl: 'ftp://wandb.test/v1' }),
119+
).rejects.toThrow('http or https');
120+
await expect(
121+
adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://user:pass@wandb.test/v1' }),
122+
).rejects.toThrow('clean API base');
123+
await expect(
124+
adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://wandb.test/v1?target=chat' }),
125+
).rejects.toThrow('clean API base');
126+
await expect(
127+
adapter.generate(ctx(), 'hello', {}, { baseUrl: 'https://wandb.test/v1#chat' }),
128+
).rejects.toThrow('clean API base');
129+
130+
expect(fetchMock).not.toHaveBeenCalled();
131+
});
132+
108133
it('includes status and response body excerpt on errors', async () => {
109134
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
110135
ok: false,

packages/ai/wandb/src/index.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ export default defineAi<Config>({
3535
};
3636
if (config.project) headers['OpenAI-Project'] = config.project;
3737

38-
const res = await fetch(`${config.baseUrl ?? DEFAULT_BASE}/chat/completions`, {
38+
const baseUrl = cleanBaseUrl(config.baseUrl ?? DEFAULT_BASE);
39+
const res = await fetch(`${baseUrl}/chat/completions`, {
3940
method: 'POST',
4041
headers,
4142
body: JSON.stringify({
@@ -72,6 +73,22 @@ export default defineAi<Config>({
7273
}),
7374
});
7475

76+
function cleanBaseUrl(value: string): string {
77+
let url: URL;
78+
try {
79+
url = new URL(value);
80+
} catch {
81+
throw new Error('W&B Inference baseUrl must be a valid URL');
82+
}
83+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
84+
throw new Error('W&B Inference baseUrl must use http or https');
85+
}
86+
if (url.username || url.password || url.search || url.hash) {
87+
throw new Error('W&B Inference baseUrl must be a clean API base without credentials, query, or hash');
88+
}
89+
return url.toString().replace(/\/+$/, '');
90+
}
91+
7592
type WandbRole = 'system' | 'user' | 'assistant' | 'tool';
7693

7794
interface WandbMessage {

0 commit comments

Comments
 (0)