| 28 |
34
|
|
expect(backendIds()).toEqual(["ox-alpha", "gemini-3.7-flash"]);
|
| 29 |
35
|
|
});
|
| 30 |
36
|
|
});
|
|
37
|
+ |
|
|
38
|
+ |
describe("choosing a backend from what the server serves", () => {
|
|
39
|
+ |
const model = (id: string, available: boolean, isDefault = false) => ({
|
|
40
|
+ |
id,
|
|
41
|
+ |
available,
|
|
42
|
+ |
isDefault,
|
|
43
|
+ |
});
|
|
44
|
+ |
|
|
45
|
+ |
it("leads with the preferred backend where the server serves it", () => {
|
|
46
|
+ |
const chosen = chooseBackend([
|
|
47
|
+ |
model("gpt-5.6-luna", true, true),
|
|
48
|
+ |
model("gemini-3.7-flash", true),
|
|
49
|
+ |
]);
|
|
50
|
+ |
expect(chosen?.id).toBe("gemini-3.7-flash");
|
|
51
|
+ |
});
|
|
52
|
+ |
|
|
53
|
+ |
it("falls to the server's own default when the preference is not served", () => {
|
|
54
|
+ |
// The case that sent every session into a 422: no deployment served a model
|
|
55
|
+ |
// by that id, and the client named it anyway.
|
|
56
|
+ |
const chosen = chooseBackend([model("gpt-5.6-luna", true, true), model("ox-alpha", false)]);
|
|
57
|
+ |
expect(chosen?.id).toBe("gpt-5.6-luna");
|
|
58
|
+ |
});
|
|
59
|
+ |
|
|
60
|
+ |
it("falls past an unavailable default to something that can answer", () => {
|
|
61
|
+ |
const chosen = chooseBackend([model("gpt-5.6-luna", false, true), model("ox-alpha", true)]);
|
|
62
|
+ |
expect(chosen?.id).toBe("ox-alpha");
|
|
63
|
+ |
});
|
|
64
|
+ |
|
|
65
|
+ |
it("honours an explicitly named model over the preference", () => {
|
|
66
|
+ |
const chosen = chooseBackend(
|
|
67
|
+ |
[model("gpt-5.6-luna", true, true), model("gemini-3.7-flash", true)],
|
|
68
|
+ |
"gpt-5.6-luna",
|
|
69
|
+ |
);
|
|
70
|
+ |
expect(chosen?.id).toBe("gpt-5.6-luna");
|
|
71
|
+ |
});
|
|
72
|
+ |
|
|
73
|
+ |
it("chooses nothing when no model has a configured credential", () => {
|
|
74
|
+ |
expect(chooseBackend([model("gpt-5.6-luna", false, true), model("ox-alpha", false)])).toBe(
|
|
75
|
+ |
undefined,
|
|
76
|
+ |
);
|
|
77
|
+ |
});
|
|
78
|
+ |
});
|
|
79
|
+ |
|
|
80
|
+ |
describe("refusing a named backend", () => {
|
|
81
|
+ |
const served = [
|
|
82
|
+ |
{ id: "gpt-5.6-luna", available: true, isDefault: true },
|
|
83
|
+ |
{ id: "ox-alpha", available: false, isDefault: false },
|
|
84
|
+ |
];
|
|
85
|
+ |
|
|
86
|
+ |
it("says a model is not served here and names what is", () => {
|
|
87
|
+ |
const refusal = refuseBackend(served, "gemini-3.7-flash");
|
|
88
|
+ |
expect(refusal).toContain("gemini-3.7-flash");
|
|
89
|
+ |
expect(refusal).toContain("gpt-5.6-luna");
|
|
90
|
+ |
});
|
|
91
|
+ |
|
|
92
|
+ |
it("separates a missing credential from a missing model", () => {
|
|
93
|
+ |
const refusal = refuseBackend(served, "ox-alpha");
|
|
94
|
+ |
expect(refusal).toContain("credential is not configured");
|
|
95
|
+ |
});
|
|
96
|
+ |
|
|
97
|
+ |
it("does not refuse a model the server serves and can run", () => {
|
|
98
|
+ |
expect(refuseBackend(served, "gpt-5.6-luna")).toBe(undefined);
|
|
99
|
+ |
});
|
|
100
|
+ |
|
|
101
|
+ |
it("says so plainly when the deployment can run nothing", () => {
|
|
102
|
+ |
const refusal = refuseBackend([{ id: "ox-alpha", available: false, isDefault: true }], "ox-alpha");
|
|
103
|
+ |
expect(refusal).toContain("no model with a configured credential");
|
|
104
|
+ |
});
|
|
105
|
+ |
});
|
|
106
|
+ |
|
|
107
|
+ |
describe("reading the published catalog", () => {
|
|
108
|
+ |
const withFetch = async (
|
|
109
|
+ |
handler: (url: string) => Response | Promise<Response>,
|
|
110
|
+ |
run: () => Promise<unknown>,
|
|
111
|
+ |
) => {
|
|
112
|
+ |
const original = globalThis.fetch;
|
|
113
|
+ |
globalThis.fetch = ((input: URL | RequestInfo) =>
|
|
114
|
+ |
Promise.resolve(handler(String(input)))) as typeof fetch;
|
|
115
|
+ |
try {
|
|
116
|
+ |
return await run();
|
|
117
|
+ |
} finally {
|
|
118
|
+ |
globalThis.fetch = original;
|
|
119
|
+ |
}
|
|
120
|
+ |
};
|
|
121
|
+ |
|
|
122
|
+ |
const api = { origin: "http://localhost:4000", token: "t" };
|
|
123
|
+ |
|
|
124
|
+ |
it("reads ids and availability from the server's own shape", async () => {
|
|
125
|
+ |
const served = await withFetch(
|
|
126
|
+ |
(url) => {
|
|
127
|
+ |
expect(url).toBe("http://localhost:4000/api/v3/models");
|
|
128
|
+ |
return new Response(
|
|
129
|
+ |
JSON.stringify({
|
|
130
|
+ |
default: "gpt-5.6-luna",
|
|
131
|
+ |
models: [
|
|
132
|
+ |
{ id: "gpt-5.6-luna", availability: "available", default: true },
|
|
133
|
+ |
{ id: "ox-alpha", availability: "unavailable", default: false },
|
|
134
|
+ |
],
|
|
135
|
+ |
}),
|
|
136
|
+ |
{ status: 200 },
|
|
137
|
+ |
);
|
|
138
|
+ |
},
|
|
139
|
+ |
() => fetchServedCatalog(api),
|
|
140
|
+ |
);
|
|
141
|
+ |
|
|
142
|
+ |
expect(served).toEqual([
|
|
143
|
+ |
{ id: "gpt-5.6-luna", available: true, isDefault: true },
|
|
144
|
+ |
{ id: "ox-alpha", available: false, isDefault: false },
|
|
145
|
+ |
]);
|
|
146
|
+ |
});
|
|
147
|
+ |
|
|
148
|
+ |
it("treats an availability word it has never seen as not available", async () => {
|
|
149
|
+ |
const served = (await withFetch(
|
|
150
|
+ |
() =>
|
|
151
|
+ |
new Response(JSON.stringify({ models: [{ id: "new", availability: "degraded" }] }), {
|
|
152
|
+ |
status: 200,
|
|
153
|
+ |
}),
|
|
154
|
+ |
() => fetchServedCatalog(api),
|
|
155
|
+ |
)) as readonly { available: boolean }[];
|
|
156
|
+ |
|
|
157
|
+ |
expect(served[0]?.available).toBe(false);
|
|
158
|
+ |
});
|
|
159
|
+ |
|
|
160
|
+ |
it("cannot answer for a server that refuses the route, rather than reporting an empty catalog", async () => {
|
|
161
|
+ |
// `undefined` is "could not ask", which falls back to the static list. An
|
|
162
|
+ |
// empty list would mean "serves nothing" and would stop the session.
|
|
163
|
+ |
for (const status of [401, 404, 500]) {
|
|
164
|
+ |
const served = await withFetch(
|
|
165
|
+ |
() => new Response("", { status }),
|
|
166
|
+ |
() => fetchServedCatalog(api),
|
|
167
|
+ |
);
|
|
168
|
+ |
expect(served).toBe(undefined);
|
|
169
|
+ |
}
|
|
170
|
+ |
});
|
|
171
|
+ |
|
|
172
|
+ |
it("cannot answer for an unreachable server", async () => {
|
|
173
|
+ |
const served = await withFetch(
|
|
174
|
+ |
() => {
|
|
175
|
+ |
throw new Error("ECONNREFUSED");
|
|
176
|
+ |
},
|
|
177
|
+ |
() => fetchServedCatalog(api),
|
|
178
|
+ |
);
|
|
179
|
+ |
expect(served).toBe(undefined);
|
|
180
|
+ |
});
|
|
181
|
+ |
});
|