.
This commit is contained in:
@@ -118,9 +118,11 @@ Models are addressed as `"driver:model"` (split on the first `:`).
|
||||
`openai` / `anthropic` / `deepseek` / `gemini` keys in settings
|
||||
(`GET/PUT/DELETE /api/me/providers`, keys stored in `user_provider_keys`,
|
||||
never returned — GET exposes presence + last4 only). `GET /api/models`
|
||||
merges the caller's own-key models into their menu, and `POST /api/chat`
|
||||
prefers the personal key for those four providers (also when the global
|
||||
driver is disabled). The admin model policy still applies. Driver
|
||||
merges the caller's own-key models into their menu (a personal entry
|
||||
replaces the platform entry with the same id, flagged `personal: true` so
|
||||
the picker lists it under its own "driver · personal key" section), and
|
||||
`POST /api/chat` prefers the personal key for those four providers (also
|
||||
when the global driver is disabled). The admin model policy still applies. Driver
|
||||
constructors accept an `apiKeyOverride` for these per-user instances.
|
||||
- `ArcaicSubDriver` ×3 (`arcaic-openai`, `arcaic-gemini`, `arcaic-qwen`,
|
||||
one model each: `chat`) + `browser.ts` engine: web-UI sessions driven by a
|
||||
|
||||
@@ -241,9 +241,12 @@ export function Chat({
|
||||
const modelGroups: PickerGroup[] = (() => {
|
||||
const byDriver = new Map<string, { value: string; label: string; hint: string }[]>();
|
||||
for (const m of models) {
|
||||
const group = m.group ?? m.driver;
|
||||
// Own-key (BYOK) models get their own section so they are never
|
||||
// confused with platform models: "openai · personal key".
|
||||
const base = m.group ?? m.driver;
|
||||
const group = m.personal ? `${base} · ${t("chat.personalTag")}` : base;
|
||||
const list = byDriver.get(group) ?? [];
|
||||
list.push({ value: m.id, label: m.label, hint: m.id });
|
||||
list.push({ value: m.id, label: m.label, hint: m.personal ? `${m.id} · ${t("chat.personalTag")}` : m.id });
|
||||
byDriver.set(group, list);
|
||||
}
|
||||
return [...byDriver.entries()].map(([group, options]) => ({ group, options }));
|
||||
|
||||
@@ -93,6 +93,7 @@ const en = {
|
||||
"chat.modelAria": "Model",
|
||||
"chat.noModels": "No models",
|
||||
"chat.selectModel": "Select model…",
|
||||
"chat.personalTag": "personal key",
|
||||
"chat.you": "You",
|
||||
"chat.greet": "How can I help, {name}?",
|
||||
"chat.greetSub": "Pick a model, then type below. Markdown is supported.",
|
||||
@@ -633,6 +634,7 @@ const es: Dict = {
|
||||
"chat.modelAria": "Modelo",
|
||||
"chat.noModels": "Sin modelos",
|
||||
"chat.selectModel": "Elegir modelo…",
|
||||
"chat.personalTag": "clave personal",
|
||||
"chat.you": "Tú",
|
||||
"chat.greet": "¿En qué te ayudo, {name}?",
|
||||
"chat.greetSub": "Elige un modelo y escribe abajo. Markdown soportado.",
|
||||
@@ -1163,6 +1165,7 @@ const fr: Dict = {
|
||||
"chat.modelAria": "Modèle",
|
||||
"chat.noModels": "Aucun modèle",
|
||||
"chat.selectModel": "Choisir un modèle…",
|
||||
"chat.personalTag": "clé personnelle",
|
||||
"chat.you": "Vous",
|
||||
"chat.greet": "Comment puis-je aider, {name} ?",
|
||||
"chat.greetSub":
|
||||
@@ -1693,6 +1696,7 @@ const ru: Dict = {
|
||||
"chat.modelAria": "Модель",
|
||||
"chat.noModels": "Нет моделей",
|
||||
"chat.selectModel": "Выбрать модель…",
|
||||
"chat.personalTag": "свой ключ",
|
||||
"chat.you": "Вы",
|
||||
"chat.greet": "Чем помочь, {name}?",
|
||||
"chat.greetSub": "Выберите модель и пишите ниже. Поддерживается Markdown.",
|
||||
@@ -2210,6 +2214,7 @@ const it: Dict = {
|
||||
"chat.modelAria": "Modello",
|
||||
"chat.noModels": "Nessun modello",
|
||||
"chat.selectModel": "Scegli modello…",
|
||||
"chat.personalTag": "chiave personale",
|
||||
"chat.you": "Tu",
|
||||
"chat.greet": "Come posso aiutare, {name}?",
|
||||
"chat.greetSub": "Scegli un modello e scrivi qui sotto. Markdown supportato.",
|
||||
@@ -2733,6 +2738,7 @@ const pt: Dict = {
|
||||
"chat.modelAria": "Modelo",
|
||||
"chat.noModels": "Sem modelos",
|
||||
"chat.selectModel": "Escolher modelo…",
|
||||
"chat.personalTag": "chave pessoal",
|
||||
"chat.you": "Tu",
|
||||
"chat.greet": "Como posso ajudar, {name}?",
|
||||
"chat.greetSub": "Escolhe um modelo e escreve abaixo. Markdown suportado.",
|
||||
|
||||
@@ -31,6 +31,8 @@ export interface DriverModel {
|
||||
driver: string;
|
||||
label: string;
|
||||
group?: string;
|
||||
/** Served through your own provider key (BYOK) — shown in its own menu section. */
|
||||
personal?: boolean;
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
|
||||
@@ -13,6 +13,9 @@ export interface DriverModel {
|
||||
label: string;
|
||||
/** Display category in the model picker (defaults to `driver`). */
|
||||
group?: string;
|
||||
/** True when served through the caller's own provider key (BYOK) rather
|
||||
* than the platform key — the UI lists these in their own section. */
|
||||
personal?: boolean;
|
||||
}
|
||||
|
||||
export interface ChatOptions {
|
||||
|
||||
+8
-7
@@ -1274,16 +1274,17 @@ const server = Bun.serve<{ ticketId: string | null; isAdmin: boolean }>({
|
||||
// mutated by the per-user merge below.
|
||||
const models = [...(await registry.listAllModelsCached(wantRefresh && admin))];
|
||||
// Personal providers (BYOK): merge the caller's own-key models.
|
||||
// Same ids as the global drivers, so already-listed ones are skipped
|
||||
// and the model picker groups them with their driver.
|
||||
// A personal entry replaces the platform entry with the same id so
|
||||
// the menu shows one clearly-marked (personal: true) section per
|
||||
// connected provider — matching /api/chat, which bills those ids
|
||||
// to the personal key.
|
||||
try {
|
||||
const personal = await listUserProviderModels(user.id);
|
||||
const seen = new Set(models.map((m) => m.id));
|
||||
const at = new Map(models.map((m, i) => [m.id, i]));
|
||||
for (const m of personal) {
|
||||
if (!seen.has(m.id)) {
|
||||
seen.add(m.id);
|
||||
models.push(m);
|
||||
}
|
||||
const idx = at.get(m.id);
|
||||
if (idx === undefined) models.push(m);
|
||||
else models[idx] = m;
|
||||
}
|
||||
} catch {
|
||||
// personal listing is best-effort — global models still served
|
||||
|
||||
@@ -99,9 +99,10 @@ export function buildUserProviderDriver(provider: UserProviderId, apiKey: string
|
||||
}
|
||||
|
||||
/**
|
||||
* List models reachable with the user's own keys. Never throws: a failing
|
||||
* provider is skipped (its driver falls back to a static list, and only a
|
||||
* total failure is swallowed here).
|
||||
* List models reachable with the user's own keys. Every entry is marked
|
||||
* `personal: true` so the UI can list them in their own menu section.
|
||||
* Never throws: a failing provider is skipped (its driver falls back to a
|
||||
* static list, and only a total failure is swallowed here).
|
||||
*/
|
||||
export async function listUserProviderModels(userId: string): Promise<DriverModel[]> {
|
||||
const out: DriverModel[] = [];
|
||||
@@ -109,7 +110,9 @@ export async function listUserProviderModels(userId: string): Promise<DriverMode
|
||||
const key = getUserProviderKey(userId, p);
|
||||
if (!key) return;
|
||||
try {
|
||||
out.push(...(await buildUserProviderDriver(p, key).listModels()));
|
||||
for (const m of await buildUserProviderDriver(p, key).listModels()) {
|
||||
out.push({ ...m, personal: true });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[providers] user ${p} listModels failed:`, (e as Error).message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user