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