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
2 changes: 2 additions & 0 deletions apps/web/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,8 @@ export const wording = {
keylessHint: `For servers that need no API key; ${productName} sends a placeholder.`,
custom: {
tile: "Custom endpoint",
modelsUnreadable:
"This provider's models could not be read, so nothing was saved.",
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.",
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/pages/custom-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -443,21 +443,25 @@ export function useCustomProviderForm(apis: Apis, original?: EditableProvider) {
</>
),
save: async (): Promise<void> => {
const models = parsedModels(
original ? original.models.map(modelDraftOf) : [draft.model],
);
if (models === null) throw new Error(copy.custom.modelsUnreadable);
if (tests) await testConnection(draft);
const tenantId = await workspaceTenantId();
const client = productClient();
const name = draft.name.trim();
if (!original) {
await client.createCustomProvider(tenantId, {
name,
models: parsedModels([draft.model])!,
models,
connection: endpointOf(draft),
});
return;
}
await client.updateCustomProvider(tenantId, original.id, {
name,
models: parsedModels(original.models.map(modelDraftOf))!,
models,
...(tests ? { connection: endpointOf(draft) } : {}),
});
},
Expand Down
14 changes: 9 additions & 5 deletions packages/client/src/client/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,19 @@ export type ModelLoginState = Readonly<
>;
export type { ModelLoginPlugin };

// CODEX_BASE_URL and XAI_OAUTH_PROXY_BASE_URL: the only endpoints these logins' tokens work on.
const LOGIN_BASE_URLS: Record<ModelLoginPlugin, string> = {
codex: "https://chatgpt.com/backend-api",
xai: "https://cli-chat-proxy.grok.com/v1",
};

const DEFAULT_BASE_URLS: Record<string, string> = {
openai: "https://api.openai.com/v1",
"openai-responses": "https://api.openai.com/v1",
anthropic: "https://api.anthropic.com/v1",
"google-genai": "https://generativelanguage.googleapis.com/v1beta",
"openai-compatible": "",
// CODEX_BASE_URL and XAI_OAUTH_PROXY_BASE_URL: the only endpoints these logins' tokens work on.
codex: "https://chatgpt.com/backend-api",
xai: "https://cli-chat-proxy.grok.com/v1",
...LOGIN_BASE_URLS,
};

function uniqueSuffix(): string {
Expand Down Expand Up @@ -205,7 +209,7 @@ export function buildCredentialsMethods(
// tenant-owned credential. A new credential name per sign-in, because a tenant-owned login
// never replaces an existing credential.
async startModelLogin(tenantId, plugin, name, signal) {
const baseURL = DEFAULT_BASE_URLS[plugin]!;
const baseURL = LOGIN_BASE_URLS[plugin];
const nativeProvider = await ensureNativeProvider(
tenantId,
plugin,
Expand Down Expand Up @@ -251,7 +255,7 @@ export function buildCredentialsMethods(
const created = (await request(`${root}/catalog/providers`, signal, {
name: `${name} ${uniqueSuffix()}`,
plugin,
baseURL: DEFAULT_BASE_URLS[plugin],
baseURL: LOGIN_BASE_URLS[plugin],
credentialId,
})) as { id?: unknown };
if (typeof created?.id !== "string")
Expand Down