Skip to content

Commit 930737a

Browse files
committed
Skip backup inference sources that fail to build
A leftover Ollama Custom URL with an extra path threw while building sibling sources, so a healthy head provider could not start. Skip per-ref build failures in the backup list and still fail when the active provider cannot build.
1 parent 3ed4165 commit 930737a

3 files changed

Lines changed: 102 additions & 6 deletions

File tree

src/config/inference-sources.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,14 @@ export function buildSourcesFromRefs(
178178
const out: InferenceSource[] = [];
179179
const seenIds = new Set<string>();
180180
for (const ref of refs) {
181-
const src = buildInferenceSourceForRef(ref, ctx, settings);
181+
let src: InferenceSource | null;
182+
try {
183+
src = buildInferenceSourceForRef(ref, ctx, settings);
184+
} catch {
185+
// A leftover sibling URL (e.g. Custom `/api/tags`) must not take down the
186+
// whole bundle. Head failure is re-checked in `buildSourceBundle`.
187+
continue;
188+
}
182189
if (src === null) continue;
183190
if (seenIds.has(src.id)) continue;
184191
seenIds.add(src.id);
@@ -215,17 +222,22 @@ function buildSourceBundle(args: {
215222

216223
const sources = buildSourcesFromRefs(refs, ctx, args.settings);
217224
const defaultId = args.head.provider;
218-
if (sources.length === 0) {
219-
const fallback = buildInferenceSourceForRef(args.head, ctx, args.settings);
225+
const hasDefault = sources.some((s) => s.id === defaultId);
226+
if (!hasDefault) {
227+
let fallback: InferenceSource | null;
228+
try {
229+
fallback = buildInferenceSourceForRef(args.head, ctx, args.settings);
230+
} catch (error) {
231+
throw new Error(`No inference source for provider "${defaultId}"`, { cause: error });
232+
}
220233
if (fallback === null) {
221234
throw new Error(`No inference source for provider "${defaultId}"`);
222235
}
223-
return { sources: [fallback], defaultSource: fallback.id };
236+
return { sources: [fallback, ...sources], defaultSource: fallback.id };
224237
}
225-
const hasDefault = sources.some((s) => s.id === defaultId);
226238
return {
227239
sources,
228-
defaultSource: hasDefault ? defaultId : (sources[0]?.id ?? defaultId),
240+
defaultSource: defaultId,
229241
};
230242
}
231243

src/provider/ollama.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ describe("ollamaOpenAIBaseURL", () => {
4141
expect(() => ollamaOpenAIBaseURL("http://localhost:11434/team")).toThrow(
4242
"expected a server root without a path",
4343
);
44+
expect(() => ollamaOpenAIBaseURL("http://localhost:11434/api/tags")).toThrow(
45+
"expected a server root without a path",
46+
);
47+
expect(() => normalizeOllamaRootURL("http://localhost:11434/api/tags")).toThrow(
48+
"expected a server root without a path",
49+
);
4450
});
4551
});
4652

tests/unit/inference-sources.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,84 @@ test("buildMainSessionSources backs the active head with other configured provid
141141
expect(bundle.defaultSource).toBe("openai");
142142
});
143143

144+
test("ollama backup with leftover extra path does not throw when OpenAI is active", () => {
145+
const leftoverURL = "http://localhost:11434/api/tags";
146+
const settings: Settings = {
147+
providers: {
148+
openai: {
149+
baseURL: "https://api.openai.com/v1",
150+
apiKey: "k",
151+
models: ["gpt-4o"],
152+
},
153+
ollama: { baseURL: leftoverURL, keyless: true, models: ["llama3"] },
154+
},
155+
};
156+
const mixedCatalog: ProviderCatalogEntry[] = [
157+
{
158+
name: "openai",
159+
baseURL: "https://api.openai.com/v1",
160+
apiKey: "k",
161+
models: ["gpt-4o"],
162+
defaultModel: "gpt-4o",
163+
},
164+
{
165+
name: "ollama",
166+
baseURL: leftoverURL,
167+
keyless: true,
168+
models: ["llama3"],
169+
defaultModel: "llama3",
170+
},
171+
];
172+
const bundle = buildMainSessionSources({
173+
settings,
174+
catalog: mixedCatalog,
175+
activeProvider: "openai",
176+
activeModel: "gpt-4o",
177+
sessionId: "sess",
178+
});
179+
expect(bundle.defaultSource).toBe("openai");
180+
expect(bundle.sources.map((s) => s.id)).toEqual(["openai"]);
181+
});
182+
183+
test("active ollama with leftover extra path still fails that provider", () => {
184+
const leftoverURL = "http://localhost:11434/api/tags";
185+
const settings: Settings = {
186+
providers: {
187+
ollama: { baseURL: leftoverURL, keyless: true, models: ["llama3"] },
188+
openai: {
189+
baseURL: "https://api.openai.com/v1",
190+
apiKey: "k",
191+
models: ["gpt-4o"],
192+
},
193+
},
194+
};
195+
const mixedCatalog: ProviderCatalogEntry[] = [
196+
{
197+
name: "ollama",
198+
baseURL: leftoverURL,
199+
keyless: true,
200+
models: ["llama3"],
201+
defaultModel: "llama3",
202+
},
203+
{
204+
name: "openai",
205+
baseURL: "https://api.openai.com/v1",
206+
apiKey: "k",
207+
models: ["gpt-4o"],
208+
defaultModel: "gpt-4o",
209+
},
210+
];
211+
expect(() =>
212+
buildMainSessionSources({
213+
settings,
214+
catalog: mixedCatalog,
215+
activeProvider: "ollama",
216+
activeModel: "llama3",
217+
sessionId: "sess",
218+
}),
219+
).toThrow('No inference source for provider "ollama"');
220+
});
221+
144222
test("legacy ollama /v1 backup does not throw when OpenAI is active", () => {
145223
const settings: Settings = {
146224
providers: {

0 commit comments

Comments
 (0)