Skip to content

Commit 1667654

Browse files
committed
fix(agents): allow default loopback service endpoints
1 parent 065873e commit 1667654

2 files changed

Lines changed: 72 additions & 3 deletions

File tree

src/agents/provider-local-service.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,39 @@ describe("provider local service", () => {
153153
await waitForProbeFailure(healthUrl);
154154
});
155155

156+
it("allows a default loopback endpoint when provider baseUrl is omitted", async () => {
157+
const port = await freePort();
158+
const healthUrl = `http://127.0.0.1:${port}/v1/models`;
159+
const acquire = createConfiguredProviderLocalServiceAcquirer(() => ({
160+
models: {
161+
providers: {
162+
"gpu-default": {
163+
models: [],
164+
localService: {
165+
command: process.execPath,
166+
args: [
167+
"-e",
168+
`const http=require("http");http.createServer((req,res)=>{res.writeHead(200);res.end("ok");}).listen(${port},"127.0.0.1");`,
169+
],
170+
healthUrl,
171+
readyTimeoutMs: 5_000,
172+
idleStopMs: 1,
173+
},
174+
},
175+
},
176+
},
177+
}));
178+
179+
const lease = await acquire({
180+
providerId: "gpu-default",
181+
baseUrl: `http://127.0.0.1:${port}/v1`,
182+
});
183+
184+
expect(lease).toBeDefined();
185+
lease?.release();
186+
await waitForProbeFailure(healthUrl);
187+
});
188+
156189
it("rejects plugin-selected local service probe hosts", async () => {
157190
const acquire = createConfiguredProviderLocalServiceAcquirer(() => ({
158191
models: {
@@ -177,6 +210,29 @@ describe("provider local service", () => {
177210
).rejects.toThrow("must match models.providers.gpu-spark.baseUrl");
178211
});
179212

213+
it("rejects a remote endpoint when provider baseUrl is omitted", async () => {
214+
const acquire = createConfiguredProviderLocalServiceAcquirer(() => ({
215+
models: {
216+
providers: {
217+
"gpu-default": {
218+
models: [],
219+
localService: {
220+
command: process.execPath,
221+
args: ["--version"],
222+
},
223+
},
224+
},
225+
},
226+
}));
227+
228+
await expect(
229+
acquire({
230+
providerId: "gpu-default",
231+
baseUrl: "http://memory.example/v1",
232+
}),
233+
).rejects.toThrow("must match models.providers.gpu-default.baseUrl");
234+
});
235+
180236
it("caps oversized local service idle stop timers", async () => {
181237
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
182238
const port = await freePort();

src/agents/provider-local-service.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,24 @@ function configuredProviderBaseUrlVariants(value: string): Set<string> {
135135
return new Set([normalized, withoutOpenAiPath, `${withoutOpenAiPath}/v1`]);
136136
}
137137

138+
function isLoopbackProviderBaseUrl(value: string): boolean {
139+
const normalized = normalizeProviderBaseUrl(value);
140+
if (!normalized) {
141+
return false;
142+
}
143+
const hostname = new URL(normalized).hostname.toLowerCase();
144+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
145+
}
146+
138147
function isConfiguredProviderBaseUrl(targetBaseUrl: string, configuredBaseUrl?: string): boolean {
139148
const target = normalizeProviderBaseUrl(targetBaseUrl);
140-
return Boolean(
141-
target && configuredBaseUrl && configuredProviderBaseUrlVariants(configuredBaseUrl).has(target),
142-
);
149+
if (!target) {
150+
return false;
151+
}
152+
const configured = configuredBaseUrl?.trim();
153+
return configured
154+
? configuredProviderBaseUrlVariants(configured).has(target)
155+
: isLoopbackProviderBaseUrl(target);
143156
}
144157

145158
/** Attach local-service startup metadata to a model without mutating the original object. */

0 commit comments

Comments
 (0)