Skip to content

Commit a42b451

Browse files
committed
feat(hub): let people add custom model providers and models
Refs CL-11917
1 parent 6e7e96c commit a42b451

30 files changed

Lines changed: 2577 additions & 188 deletions

‎apps/hub/src/app.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ export async function createHubRuntime(
561561
credentialCipher,
562562
config,
563563
installRootId,
564+
async (request) => app.fetch(request),
564565
),
565566
);
566567
const modelLogins = mountModelLogins(app, {

‎apps/hub/src/artifacts/service.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import {
131131
} from "../config/preferences.ts";
132132
import { awaitingApproval } from "../projects/deploy-tools.ts";
133133
import { frozenModelChoice, workflowBindings } from "../config/models.ts";
134+
import { modelDirectory } from "../settings/custom-providers.ts";
134135
import { materialsNote } from "../config/materials.ts";
135136
import { servingOffering } from "../workflows/install-for-tenant.ts";
136137
import {
@@ -999,13 +1000,15 @@ export function createArtifactService(
9991000
): Promise<AcceptedMaterials> {
10001001
await requireProject(actor, projectId, "read");
10011002
const { models } = await loadSettings(rawDB, actor.tenantId, projectId);
1002-
const choice = frozenModelChoice(models.value);
1003+
const directory = await modelDirectory(rawDB, actor.tenantId);
1004+
const choice = frozenModelChoice(directory, models.value);
10031005
const offering =
10041006
choice &&
10051007
(await servingOffering(
10061008
rawDB,
1009+
directory,
10071010
actor.tenantId,
1008-
workflowBindings(choice).coordinator,
1011+
workflowBindings(directory, choice).coordinator,
10091012
));
10101013
return acceptedMaterials(offering?.capabilities ?? []);
10111014
},

‎apps/hub/src/availability.ts‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ const log = getLogger(["corbits-builder", "availability"]);
2929

3030
// These are wire-protocol adapters, not a provider/model registry. Identity,
3131
// endpoints, shadowing and credential authority all come from the native catalog.
32-
function probeRequest(plugin: string, baseURL: string, secret: string) {
32+
function probeRequest(
33+
plugin: string,
34+
baseURL: string,
35+
secret: string | undefined,
36+
) {
3337
const url = new URL(baseURL);
3438
if (
3539
!["http:", "https:"].includes(url.protocol) ||
@@ -45,16 +49,17 @@ function probeRequest(plugin: string, baseURL: string, secret: string) {
4549
case "openai-responses":
4650
case "openai-compatible":
4751
url.pathname = `${url.pathname.replace(/\/$/, "")}/models`;
48-
headers.set("authorization", `Bearer ${secret}`);
52+
if (secret !== undefined)
53+
headers.set("authorization", `Bearer ${secret}`);
4954
break;
5055
case "anthropic":
5156
url.pathname = `${url.pathname.replace(/\/$/, "")}/v1/models`;
52-
headers.set("x-api-key", secret);
57+
if (secret !== undefined) headers.set("x-api-key", secret);
5358
headers.set("anthropic-version", "2023-06-01");
5459
break;
5560
case "google-genai":
5661
url.pathname = `${url.pathname.replace(/\/$/, "")}/v1beta/models`;
57-
headers.set("x-goog-api-key", secret);
62+
if (secret !== undefined) headers.set("x-goog-api-key", secret);
5863
break;
5964
default:
6065
return null;
@@ -139,11 +144,14 @@ export function createAvailabilityProbe(
139144
};
140145
}
141146

142-
/** One model-list call with this key; `origin`, when given, is the only origin the key may reach. */
147+
/**
148+
* One model-list call with this key, or with none for a keyless server; `origin`, when given, is
149+
* the only origin the key may reach.
150+
*/
143151
export async function probeKey(
144152
plugin: string,
145153
baseURL: string,
146-
secret: string,
154+
secret: string | undefined,
147155
origin?: string,
148156
): Promise<AvailabilityState> {
149157
let request;

‎apps/hub/src/config/models.test.ts‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,34 @@
11
import { expect, test } from "bun:test";
2-
import { resolveModelChoice } from "./models.ts";
2+
import { MODEL_PROVIDERS, resolveModelChoice } from "./models.ts";
33

44
test("a project runs on the only connected provider's profile, a chosen one, or none", () => {
5-
expect(resolveModelChoice(null, ["anthropic"])).toMatchObject({
5+
expect(
6+
resolveModelChoice(MODEL_PROVIDERS, null, ["anthropic"]),
7+
).toMatchObject({
68
choice: { profile: "anthropic_default" },
79
});
8-
expect(resolveModelChoice(null, ["anthropic", "openai"])).toEqual({
10+
expect(
11+
resolveModelChoice(MODEL_PROVIDERS, null, ["anthropic", "openai"]),
12+
).toEqual({
913
refused: "model_profile_required",
1014
});
11-
expect(resolveModelChoice(null, [])).toEqual({
15+
expect(resolveModelChoice(MODEL_PROVIDERS, null, [])).toEqual({
1216
refused: "model_provider_missing",
1317
});
1418
expect(
15-
resolveModelChoice({ profile: "openai_default" }, ["anthropic", "openai"]),
19+
resolveModelChoice(MODEL_PROVIDERS, { profile: "openai_default" }, [
20+
"anthropic",
21+
"openai",
22+
]),
1623
).toMatchObject({
1724
choice: { profile: "openai_default" },
1825
});
1926
// A stored choice this server no longer offers is no choice.
2027
expect(
21-
resolveModelChoice({ profile: "gone_default" }, ["anthropic", "openai"]),
28+
resolveModelChoice(MODEL_PROVIDERS, { profile: "gone_default" }, [
29+
"anthropic",
30+
"openai",
31+
]),
2232
).toEqual({
2333
refused: "model_profile_required",
2434
});

0 commit comments

Comments
 (0)