Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 3 additions & 21 deletions apps/hub/src/config/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ type CommonEntry = {
plugin: string;
label: string;
hint: string;
// Whether a person points it at their own base URL.
needsBaseUrl: boolean;
baseURL: string;
// The adapter quirks every offering of this provider carries.
quirks?: Readonly<Record<string, unknown>>;
Expand Down Expand Up @@ -66,7 +64,6 @@ export const MODEL_PROVIDERS: readonly ShippedEntry[] = [
kind: "api_key",
label: "OpenAI",
hint: "An OpenAI API key.",
needsBaseUrl: false,
baseURL: "https://api.openai.com/v1",
models: [
{ id: "gpt-6-astra", label: "GPT-6 Astra" },
Expand All @@ -84,7 +81,6 @@ export const MODEL_PROVIDERS: readonly ShippedEntry[] = [
kind: "api_key",
label: "Anthropic",
hint: "An Anthropic API key.",
needsBaseUrl: false,
baseURL: "https://api.anthropic.com",
models: [
{ id: "claude-fable-5-1", label: "Claude Fable 5.1" },
Expand All @@ -106,7 +102,6 @@ export const MODEL_PROVIDERS: readonly ShippedEntry[] = [
kind: "api_key",
label: "Google",
hint: "A Gemini API key.",
needsBaseUrl: false,
baseURL: "https://generativelanguage.googleapis.com",
models: [
{ id: "gemini-4-argon", label: "Gemini 4 Argon" },
Expand All @@ -121,7 +116,6 @@ export const MODEL_PROVIDERS: readonly ShippedEntry[] = [
quirks: { reasoning: { maxEffort: "xhigh" } },
label: "xAI API",
hint: "An xAI API key, from console.x.ai.",
needsBaseUrl: false,
baseURL: "https://api.x.ai/v1",
models: [
{ id: "grok-4.7", label: "Grok 4.7" },
Expand All @@ -140,7 +134,6 @@ export const MODEL_PROVIDERS: readonly ShippedEntry[] = [
kind: "login",
label: "ChatGPT",
hint: "Your ChatGPT subscription, signed in through OpenAI.",
needsBaseUrl: false,
baseURL: "https://chatgpt.com/backend-api",
models: [
{ id: "gpt-6.1-sol", label: "gpt-6.1-sol" },
Expand All @@ -160,7 +153,6 @@ export const MODEL_PROVIDERS: readonly ShippedEntry[] = [
kind: "login",
label: "xAI",
hint: "Your xAI subscription, signed in through xAI.",
needsBaseUrl: false,
baseURL: "https://cli-chat-proxy.grok.com/v1",
models: [
{ id: "grok-4.7", label: "Grok 4.7" },
Expand Down Expand Up @@ -190,23 +182,13 @@ export function modelProvider(
return directory.find((entry) => entry.id === id);
}

/**
* The shipped entry a catalog provider row is a connection of: the one at its plugin and fixed base
* URL, else the entry of that plugin a person points at their own base URL.
*/
/** The shipped entry a catalog provider row is a connection of: the one at its plugin and base URL. */
export function shippedOfRow(
row: Readonly<{ plugin: string; baseURL: string | undefined }>,
): ShippedEntry | undefined {
const ofPlugin = MODEL_PROVIDERS.filter(
(entry) => entry.plugin === row.plugin,
return MODEL_PROVIDERS.find(
(entry) => entry.plugin === row.plugin && entry.baseURL === row.baseURL,
);
if (row.baseURL !== undefined) {
const exact = ofPlugin.find(
(entry) => !entry.needsBaseUrl && entry.baseURL === row.baseURL,
);
if (exact) return exact;
}
return ofPlugin.find((entry) => entry.needsBaseUrl);
}

/** The custom provider running on a catalog provider row, else the shipped entry it connects. */
Expand Down
1 change: 0 additions & 1 deletion apps/hub/src/settings/custom-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ export async function modelDirectory(
keyless: entry.keyless,
label: entry.name,
hint: CUSTOM_APIS[CustomApi.assert(row.plugin)],
needsBaseUrl: true,
baseURL: row.baseURL,
models,
},
Expand Down
6 changes: 2 additions & 4 deletions apps/hub/src/settings/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,6 @@ export function createSettingsRoutes(
kind: entry.kind,
label: entry.label,
hint: entry.hint,
needsBaseUrl: entry.needsBaseUrl,
baseURL: entry.baseURL,
models: [...entry.models],
// A stored row name is the catalog's unique key, not something a person chose.
Expand Down Expand Up @@ -378,15 +377,14 @@ export function createSettingsRoutes(
const input = ProviderTest(await c.req.json().catch(() => null));
if (input instanceof type.errors)
throw new ProjectError(400, "invalid_provider_test");
const typed = input.baseURL?.trim();
const entry = shippedOfRow({ plugin: input.plugin, baseURL: typed });
const baseURL = input.baseURL?.trim();
const entry = shippedOfRow({ plugin: input.plugin, baseURL });
if (!entry && !CustomApi.allows(input.plugin))
throw new ProjectError(400, "unknown_provider");
if (entry?.kind === "login")
throw new ProjectError(400, "provider_signs_in");
if (entry && !("secret" in input))
throw new ProjectError(400, "secret_required");
const baseURL = entry && !entry.needsBaseUrl ? entry.baseURL : typed;
if (!baseURL) throw new ProjectError(400, "base_url_required");
return c.json({
status: await probeKey(
Expand Down
64 changes: 63 additions & 1 deletion apps/web/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,69 @@ export const wording = {
provider: "Provider",
baseUrl: "Base URL",
key: "API key",
keyless: `For servers that need no API key; ${productName} sends a placeholder.`,
keyless: "Keyless",
keylessAbout: "About Keyless",
keylessHint: `For servers that need no API key; ${productName} sends a placeholder.`,
custom: {
tile: "Custom endpoint",
tileHint: "Your own server, on the API it speaks.",
addTitle: "Add a custom endpoint",
addLead: "Connect your own server. It is saved once the test passes.",
name: "Name",
api: "API type",
apiLead: "Pick the API your server speaks.",
apiPaths: {
"openai-compatible": "/v1/chat/completions",
"openai-responses": "/v1/responses",
anthropic: "/v1/messages",
},
apiTips: {
"openai-compatible":
"OpenAI's chat format. vLLM, Ollama, LM Studio, llama.cpp and most hosted servers use it.",
"openai-responses":
"OpenAI's newer Responses format. OpenAI and a few servers that copy it use it.",
anthropic:
"Anthropic's Messages format. Anthropic and servers that copy it use it.",
},
tips: {
name: `What this server is called in ${productName}.`,
api: "The request format your server understands.",
baseUrl: "The address requests go to, usually ending in /v1.",
key: "The key your server asks for. Kept encrypted and never shown again.",
modelId: "The exact model name your server expects.",
modelName: "How the model is shown. Empty shows its model ID.",
contextWindow: "How many tokens the model can read at once.",
maxOutputTokens:
"The most tokens it writes in one reply. Below the context window.",
},
about: (label: string) => `About ${label}`,
models: "Models",
modelCount: (count: number) =>
count === 1 ? "1 model" : `${count} models`,
duplicateIds: "Each model needs its own model ID.",
modelId: "Model ID",
modelName: "Name",
optional: "Optional",
contextWindow: "Context window",
maxOutputTokens: "Output cap",
addModel: "+ Add model",
removeModel: (id: string) => `Remove ${id}`,
keyAgain: "A new API type or base URL needs its key again, or Keyless.",
editTitle: (label: string) => `Edit ${label}`,
editLead:
"Change its name or connection. A new API type, base URL or key is tested before it is saved.",
options: (label: string) => `Options for ${label}`,
edit: "Edit",
remove: "Remove…",
confirmRemove: "Yes, remove it",
back: "Back",
save: "Save",
saving: "Saving…",
savedState: "Saved",
failedState: "Not saved",
saved: (label: string) => `${label} saved`,
removed: (label: string) => `${label} removed`,
},
test: "Test connection",
testing: "Testing…",
saved: (label: string) => `${label} connected`,
Expand Down
Loading