diff --git a/.env.example b/.env.example
index 89e4709..8ae0ca5 100644
--- a/.env.example
+++ b/.env.example
@@ -1,20 +1,23 @@
-POSTGRES_HOST=localhost
-POSTGRES_PORT=5432
-POSTGRES_DB=oxidechat
-POSTGRES_USER=postgres
-POSTGRES_PASSWORD=your_password_here
-
-HOST=0.0.0.0
-PORT=8080
-BASE_URL=http://localhost:8080
-
-# OAuth Configuration (optional - can also be configured via admin UI)
-OAUTH_GOOGLE_CLIENT_ID=
-OAUTH_GOOGLE_CLIENT_SECRET=
-
-OAUTH_DISCORD_CLIENT_ID=
-OAUTH_DISCORD_CLIENT_SECRET=
-
-# API Key Encryption (optional - 64 hex chars = 32 bytes)
+POSTGRES_HOST=localhost
+POSTGRES_PORT=5432
+POSTGRES_DB=oxidechat
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=your_password_here
+
+HOST=0.0.0.0
+PORT=8080
+BASE_URL=http://localhost:8080
+
+# OAuth Configuration (optional - can also be configured via admin UI)
+OAUTH_GOOGLE_CLIENT_ID=
+OAUTH_GOOGLE_CLIENT_SECRET=
+
+OAUTH_DISCORD_CLIENT_ID=
+OAUTH_DISCORD_CLIENT_SECRET=
+
+# Credential encryption
+# A key is generated automatically in OXIDECHAT_DATA_DIR/master.key.
+OXIDECHAT_DATA_DIR=./.data
+# For containers or multiple replicas, provide the same explicit 64-character hex key to every instance.
# Generate with: openssl rand -hex 32
-# ENCRYPTION_KEY=your_64_char_hex_key_here
\ No newline at end of file
+# ENCRYPTION_KEY=your_64_character_hex_key_here
diff --git a/.intent/.gitignore b/.intent/.gitignore
deleted file mode 100644
index 68aa4c6..0000000
--- a/.intent/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-# Intent workspace config directory
-# Only config.json is tracked in git — everything else is local
-*
-!.gitignore
-config.json
diff --git a/AGENTS.md b/AGENTS.md
index 9fe5383..a0673c7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -38,7 +38,7 @@
- All request/response structs live in `src/types/`, not in the code files files
- Organized by domain: `i18n.rs`, `base.rs`, etc.
-- `mod.rs` re-exports all types for easy importing: `use crate::types::*;`
+- `providers_billing` re-exports all types for easy importing: `use crate::types::*;`
- Each file uses section comments for clarity:
- Request Types (structs with `Deserialize`)
- Response Types (structs with `Serialize`)
diff --git a/Cargo.lock b/Cargo.lock
index 80f2f8f..294496c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2248,7 +2248,7 @@ dependencies = [
[[package]]
name = "omniference"
-version = "0.3.0"
+version = "0.3.2"
dependencies = [
"anyhow",
"async-stream",
diff --git a/frontend/components/settings/DefaultModelPicker.vue b/frontend/components/settings/DefaultModelPicker.vue
index a157cf9..771fca9 100644
--- a/frontend/components/settings/DefaultModelPicker.vue
+++ b/frontend/components/settings/DefaultModelPicker.vue
@@ -59,7 +59,7 @@
v-for="model in pickerModels"
:key="model.id"
class="px-3 py-2 hover:bg-accent/50 cursor-pointer transition-colors border-b border-border/50 last:border-0"
- :class="modelValueFor(model) === modelValue ? 'bg-accent/30' : ''"
+ :class="modelValueFor(model) === modelValue ? 'bg-accent/30' : ''"
@click="selectModel(model)"
>
diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue
index 0cc25a0..995f315 100644
--- a/frontend/layouts/default.vue
+++ b/frontend/layouts/default.vue
@@ -53,6 +53,15 @@ const checkAuth = async () => {
if (store.bootState !== 'online') {
await store.getBaseData();
}
+
+ if (!store.auth.user && !store.auth.isAuthenticated) {
+ if (!getMePromise) {
+ getMePromise = store.getMe().finally(() => {
+ getMePromise = null;
+ });
+ }
+ await getMePromise;
+ }
if (store.base?.needs_setup) {
if (route.path !== '/auth/setup') {
@@ -67,15 +76,6 @@ const checkAuth = async () => {
return {authenticated: false};
}
- if (!store.auth.user && !store.auth.isAuthenticated) {
- if (!getMePromise) {
- getMePromise = store.getMe().finally(() => {
- getMePromise = null;
- });
- }
- await getMePromise;
- }
-
const isAuthPage = route.path.startsWith('/auth');
if (store.auth.isAuthenticated && isAuthPage) {
diff --git a/frontend/pages/settings/admin-config.vue b/frontend/pages/settings/admin-config.vue
index 4b7ed0e..e2d154d 100644
--- a/frontend/pages/settings/admin-config.vue
+++ b/frontend/pages/settings/admin-config.vue
@@ -28,9 +28,11 @@
{{ store.getTranslation('settings.admin.default_model_hint') }}
@@ -50,7 +52,7 @@ const {$customFetch} = useNuxtApp();
const saving = ref(false);
const enableProviderSelector = ref(store.base?.enable_provider_selector ?? false);
const allowServerStdioMcp = ref(store.base?.allow_server_stdio_mcp ?? false);
-const defaultModelKey = ref(store.base?.default_model_key ?? null);
+const defaultModelId = ref(store.base?.default_model_id ?? null);
async function toggleProviderSelector(val: boolean) {
const previous = enableProviderSelector.value;
@@ -89,19 +91,19 @@ async function toggleAllowStdioMcp(val: boolean) {
}
async function setDefaultModel(val: string | null) {
- const previous = defaultModelKey.value;
- const newKey = val;
- defaultModelKey.value = newKey;
+ const previous = defaultModelId.value;
+ const newId = val;
+ defaultModelId.value = newId;
saving.value = true;
try {
await $customFetch('/api/v1/admin/config', {
method: 'PATCH',
- body: {default_model_key: newKey},
+ body: {default_model_id: newId},
});
- if (store.base) store.base.default_model_key = newKey;
+ if (store.base) store.base.default_model_id = newId;
} catch (error) {
- console.error('Failed to update default model key:', error);
- defaultModelKey.value = previous;
+ console.error('Failed to update default model:', error);
+ defaultModelId.value = previous;
} finally {
saving.value = false;
}
diff --git a/frontend/pages/settings/providers.vue b/frontend/pages/settings/providers.vue
index e53dfcc..64af241 100644
--- a/frontend/pages/settings/providers.vue
+++ b/frontend/pages/settings/providers.vue
@@ -1,522 +1,764 @@
-
-
-
-
-
{{ store.getTranslation('settings.providers.title') }}
-
{{ store.getTranslation('settings.providers.description') }}
-
-
-
- {{ store.getTranslation('settings.providers.add_custom') }}
-
-
-
-
-
-
-
-
-
-
-
![]()
-
-
-
-
-
-
{{ item.name }}
-
- {{ store.getTranslation('settings.providers.configured') }}
-
-
-
{{ item.description || item.base_url }}
-
-
-
-
-
- toggleProvider(item, val)" />
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
{{ store.getTranslation('settings.providers.title') }}
+
{{ store.getTranslation('settings.providers.description') }}
+
+
+
+ {{ store.getTranslation('settings.providers.add_custom') }}
+
+
+
+
+
+
+
{{ store.getTranslation('settings.providers.billing.tab') }}
+
{{ store.getTranslation('settings.providers.billing.local_tracked') }}
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
{{ item.name }}
+
{{ store.getTranslation(billingStatusKey(item.billing)) }}
+
+
+
+
+ {{ store.getTranslation('settings.providers.billing.stale') }}
+
+
+
+
+
+
+
{{ upstreamSummary(item.billing) }}
+
{{ upstreamDetail(item.billing) }}
+
+
+
{{ store.getTranslation(billingStatusKey(item.billing)) }}
+
+
+ {{ updatedLabel(item.billing.last_synced_at) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
{{ item.name }}
+
+ {{ store.getTranslation('settings.providers.configured') }}
+
+
+
{{ item.description || item.base_url }}
+
+
+
+
+
+ toggleProvider(item, val)" />
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/pages/settings/teams.vue b/frontend/pages/settings/teams.vue
index b18d539..481674e 100644
--- a/frontend/pages/settings/teams.vue
+++ b/frontend/pages/settings/teams.vue
@@ -125,11 +125,13 @@
{{ store.getTranslation('settings.teams.default_model') }}
{{ store.getTranslation('settings.teams.default_model_hint') }}
(teamDefaultModelKey = v)"
+ @update:model-value="v => (teamDefaultModelId = v)"
/>
@@ -323,7 +325,7 @@ const createOpen = ref(false);
const deleteOpen = ref(false);
const form = reactive({name: '', description: '', allow_all_models: false});
-const teamDefaultModelKey = ref
(null);
+const teamDefaultModelId = ref(null);
const createForm = reactive({name: '', description: '', allow_all_models: false});
const selectedMemberIds = ref([]);
const selectedProviderIds = ref([]);
@@ -439,7 +441,7 @@ function applySelected(team: TeamDetailed) {
form.name = team.name;
form.description = team.description ?? '';
form.allow_all_models = team.allow_all_models;
- teamDefaultModelKey.value = team.default_model_key ?? null;
+ teamDefaultModelId.value = team.default_model_id ?? null;
selectedMemberIds.value = team.members.map(member => member.id);
selectedProviderIds.value = [...team.model_access.provider_ids];
selectedModelIds.value = [...team.model_access.model_ids];
@@ -527,7 +529,7 @@ async function saveAll() {
await $customFetch(`/api/v1/admin/teams/${id}`, {
method: 'PATCH',
- body: {name: form.name, description: form.description ? form.description : null, allow_all_models: form.allow_all_models, default_model_key: teamDefaultModelKey.value},
+ body: {name: form.name, description: form.description ? form.description : null, allow_all_models: form.allow_all_models, default_model_id: teamDefaultModelId.value},
});
await $customFetch(`/api/v1/admin/teams/${id}/members`, {
diff --git a/frontend/stores/index.ts b/frontend/stores/index.ts
index 21bde10..f752ce2 100644
--- a/frontend/stores/index.ts
+++ b/frontend/stores/index.ts
@@ -51,7 +51,7 @@ interface Base {
roles: Role[];
enable_provider_selector: boolean;
allow_server_stdio_mcp: boolean;
- default_model_key: string | null;
+ default_model_id: string | null;
}
export const useMainStore = defineStore('main', {
@@ -203,6 +203,7 @@ export const useMainStore = defineStore('main', {
},
async setup(email: string, username: string, password: string) {
const {$customFetch} = useNuxtApp();
+ const chatStore = useChatStore();
const response = await $customFetch<{user: User}>('/api/v1/auth/setup', {
method: 'POST',
body: {email, username, password},
@@ -211,6 +212,7 @@ export const useMainStore = defineStore('main', {
this.auth.user = response.user;
this.auth.isAuthenticated = true;
await this.getBaseData();
+ await chatStore.init();
}
return response;
},
diff --git a/frontend/stores/providerBillingStore.ts b/frontend/stores/providerBillingStore.ts
new file mode 100644
index 0000000..7ece606
--- /dev/null
+++ b/frontend/stores/providerBillingStore.ts
@@ -0,0 +1,50 @@
+import {defineStore} from 'pinia';
+import type {ProviderBillingOverview, UpdateProviderBillingPayload} from '~/types/providers';
+
+export const useProviderBillingStore = defineStore('providerBilling', {
+ state: () => ({
+ overviews: {} as Record,
+ loading: {} as Record,
+ errors: {} as Record,
+ listLoading: false,
+ }),
+ actions: {
+ async fetchBillingOverviews() {
+ const {$customFetch} = useNuxtApp();
+ this.listLoading = true;
+ try {
+ const rows = await $customFetch('/api/v1/admin/providers/billing');
+ this.overviews = Object.fromEntries((rows ?? []).map(row => [row.provider_id, row]));
+ } finally {
+ this.listLoading = false;
+ }
+ },
+ async fetchProviderBilling(providerId: string) {
+ return this.run(providerId, () => useNuxtApp().$customFetch(`/api/v1/admin/providers/${providerId}/billing`));
+ },
+ async updateProviderBilling(providerId: string, payload: UpdateProviderBillingPayload) {
+ return this.run(providerId, () => useNuxtApp().$customFetch(`/api/v1/admin/providers/${providerId}/billing`, {method: 'PUT', body: payload}));
+ },
+ async removeProviderBilling(providerId: string) {
+ await this.run(providerId, () => useNuxtApp().$customFetch(`/api/v1/admin/providers/${providerId}/billing`, {method: 'DELETE'}), false);
+ delete this.overviews[providerId];
+ },
+ async refreshProviderBilling(providerId: string) {
+ return this.run(providerId, () => useNuxtApp().$customFetch(`/api/v1/admin/providers/${providerId}/billing/refresh`, {method: 'POST'}));
+ },
+ async run(providerId: string, action: () => Promise, storeResult = true) {
+ this.loading[providerId] = true;
+ this.errors[providerId] = null;
+ try {
+ const result = await action();
+ if (storeResult && result) this.overviews[providerId] = result;
+ return result;
+ } catch (error: any) {
+ this.errors[providerId] = error?.message ?? String(error);
+ throw error;
+ } finally {
+ this.loading[providerId] = false;
+ }
+ },
+ },
+});
diff --git a/frontend/types/chat.ts b/frontend/types/chat.ts
index 367049d..e146bc2 100644
--- a/frontend/types/chat.ts
+++ b/frontend/types/chat.ts
@@ -28,7 +28,7 @@ export interface TeamMember {
}
export interface TeamDetailed extends Omit {
- default_model_key: string | null;
+ default_model_id: string | null;
members: TeamMember[];
model_access: {
provider_ids: string[];
diff --git a/frontend/types/providers.ts b/frontend/types/providers.ts
new file mode 100644
index 0000000..c801ed5
--- /dev/null
+++ b/frontend/types/providers.ts
@@ -0,0 +1,36 @@
+export type ProviderBillingStatus = 'NOT_CONFIGURED' | 'AVAILABLE' | 'UNSUPPORTED' | 'UNAUTHORIZED' | 'UPSTREAM_ERROR';
+export type ProviderBillingMetricKind = 'CREDIT_BALANCE' | 'KEY_LIMIT' | 'SPEND_THRESHOLD' | 'SPEND_ONLY';
+
+export interface ProviderBillingMetric {
+ metric_kind: ProviderBillingMetricKind;
+ currency: string;
+ period_start: string | null;
+ period_end: string | null;
+ limit_amount: string | number | null;
+ spent_amount: string | number | null;
+ remaining_amount: string | number | null;
+ is_hard_limit: boolean;
+ thresholds: Array;
+}
+
+export interface ProviderBillingOverview {
+ provider_id: string;
+ provider_kind: string;
+ status: ProviderBillingStatus;
+ is_enabled: boolean;
+ has_billing_credential: boolean;
+ upstream: ProviderBillingMetric | null;
+ local: {
+ currency: string;
+ period_start: string;
+ period_end: string;
+ spent_amount: string | number;
+ };
+ last_synced_at: string | null;
+ is_stale: boolean;
+ error_code: string | null;
+}
+
+export interface UpdateProviderBillingPayload {
+ credential?: string;
+}
diff --git a/migrations/20251226000000_initial_schema.sql b/migrations/20251226000000_initial_schema.sql
index 97605f5..b8dabdb 100644
--- a/migrations/20251226000000_initial_schema.sql
+++ b/migrations/20251226000000_initial_schema.sql
@@ -1,5 +1,5 @@
--- Initial schema for OxideChat
--- This migration creates the core tables for the chat application
+-- Clean initial schema for OxideChat
+-- This migration creates the complete schema and seed data for a new installation.
-- Translations table - stores all i18n translations
CREATE TABLE IF NOT EXISTS i18n_translations (
@@ -71,6 +71,7 @@ CREATE TABLE IF NOT EXISTS app_config (
INSERT INTO app_config (key, value) VALUES
('language', 'en'),
('allow_user_providers', 'false'),
+ ('allow_server_stdio_mcp', 'false'),
('default_theme', '{}'::jsonb)
ON CONFLICT (key) DO NOTHING;
@@ -275,7 +276,8 @@ CREATE TABLE IF NOT EXISTS messages (
content_parts JSONB DEFAULT '[]',
cost_details JSONB DEFAULT '{}',
usage_details JSONB DEFAULT '{}',
- reasoning_details JSONB DEFAULT '{}'
+ reasoning_details JSONB DEFAULT '{}',
+ request_settings JSONB NOT NULL DEFAULT '{}'
);
-- Add the chats -> messages FK idempotently (messages table is created above in the same migration).
@@ -300,6 +302,8 @@ CREATE TABLE IF NOT EXISTS user_preferences (
use_remend BOOLEAN DEFAULT true,
theme_css_vars JSONB DEFAULT '{}',
custom_theme_urls JSONB DEFAULT '[]',
+ default_provider_slug VARCHAR(255),
+ default_tools JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
@@ -312,6 +316,7 @@ CREATE TABLE IF NOT EXISTS images (
mime_type VARCHAR(64) NOT NULL DEFAULT 'image/png',
size_bytes BIGINT NOT NULL,
source VARCHAR(50), -- 'imagegen', 'upload', etc.
+ caption TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
@@ -330,7 +335,12 @@ INSERT INTO permissions (name, description) VALUES
('admin.config.view', 'View application config'),
('admin.config.edit', 'Edit application config'),
('admin.providers.view', 'View AI provider configuration'),
- ('admin.providers.edit', 'Configure AI providers')
+ ('admin.providers.edit', 'Configure AI providers'),
+ ('admin.teams.view', 'View teams'),
+ ('admin.teams.edit', 'Manage teams'),
+ ('admin.budgets.view', 'View budget configurations'),
+ ('admin.budgets.edit', 'Manage budget configurations'),
+ ('admin.analytics.view', 'View usage analytics')
ON CONFLICT (name) DO NOTHING;
-- Assign permissions to user role (basic settings)
@@ -394,6 +404,7 @@ CREATE TABLE IF NOT EXISTS tools (
-- Custom settings schema (API keys, user config) - NOT passed to LLM
settings_schema JSONB DEFAULT '{}',
+ system_prompt TEXT,
-- Permissions
is_enabled BOOLEAN DEFAULT true,
@@ -468,6 +479,258 @@ CREATE TABLE IF NOT EXISTS tool_executions (
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
+-- Gateway catalog
+
+DO $$ BEGIN
+ CREATE TYPE gateway_availability AS ENUM (
+ 'AVAILABLE',
+ 'USER_UNAVAILABLE'
+ );
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+CREATE TABLE IF NOT EXISTS gateway_catalog_models (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ provider_id UUID NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
+ source_gateway VARCHAR(50) NOT NULL, -- e.g. 'openrouter'
+ gateway_model_id TEXT NOT NULL, -- bare 'author/slug'
+ local_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
+ display_name TEXT NOT NULL DEFAULT '',
+ availability_state gateway_availability NOT NULL DEFAULT 'AVAILABLE',
+ reason TEXT,
+ raw JSONB NOT NULL DEFAULT '{}',
+ fetched_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE (provider_id, source_gateway, gateway_model_id)
+);
+
+CREATE TABLE IF NOT EXISTS gateway_model_provider_options (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ catalog_model_id UUID NOT NULL REFERENCES gateway_catalog_models(id) ON DELETE CASCADE,
+ provider_slug TEXT, -- endpoint tag, e.g. 'openai'
+ provider_name TEXT,
+ endpoint_name TEXT,
+ status DOUBLE PRECISION, -- OpenRouter health status (0 = healthy)
+ quantization TEXT,
+ context_length INTEGER,
+ max_completion_tokens INTEGER,
+ latency DOUBLE PRECISION, -- p50 latency (seconds)
+ throughput DOUBLE PRECISION, -- p50 throughput (tokens/sec)
+ uptime DOUBLE PRECISION, -- uptime over last 30m (percent)
+ price_input DOUBLE PRECISION, -- USD per million input tokens
+ price_output DOUBLE PRECISION, -- USD per million output tokens
+ raw JSONB NOT NULL DEFAULT '{}',
+ fetched_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_gateway_catalog_models_provider ON gateway_catalog_models(provider_id);
+CREATE INDEX IF NOT EXISTS idx_gateway_catalog_models_local ON gateway_catalog_models(local_model_id);
+CREATE INDEX IF NOT EXISTS idx_gateway_catalog_models_availability ON gateway_catalog_models(availability_state);
+CREATE INDEX IF NOT EXISTS idx_gateway_provider_options_catalog ON gateway_model_provider_options(catalog_model_id);
+
+
+-- Teams
+CREATE TABLE IF NOT EXISTS teams (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name VARCHAR(100) NOT NULL UNIQUE,
+ description TEXT,
+ is_default BOOLEAN NOT NULL DEFAULT false,
+ allow_all_models BOOLEAN NOT NULL DEFAULT false,
+ budget_id UUID,
+ default_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS teams_single_default ON teams (is_default) WHERE is_default = true;
+
+CREATE TABLE IF NOT EXISTS team_members (
+ team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (team_id, user_id)
+);
+
+CREATE TABLE IF NOT EXISTS team_model_access (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
+ provider_id UUID REFERENCES providers(id) ON DELETE CASCADE,
+ model_id UUID REFERENCES models(id) ON DELETE CASCADE,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ CONSTRAINT team_model_access_target_check CHECK (
+ (provider_id IS NOT NULL AND model_id IS NULL) OR
+ (provider_id IS NULL AND model_id IS NOT NULL)
+ )
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS team_model_access_provider_unique ON team_model_access (team_id, provider_id) WHERE provider_id IS NOT NULL;
+CREATE UNIQUE INDEX IF NOT EXISTS team_model_access_model_unique ON team_model_access (team_id, model_id) WHERE model_id IS NOT NULL;
+
+CREATE TABLE IF NOT EXISTS team_permissions (
+ team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
+ permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
+ is_allowed BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (team_id, permission_id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_teams_default ON teams(is_default) WHERE is_default = true;
+CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
+CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
+CREATE INDEX IF NOT EXISTS idx_team_model_access_team ON team_model_access(team_id);
+CREATE INDEX IF NOT EXISTS idx_team_model_access_provider ON team_model_access(provider_id);
+CREATE INDEX IF NOT EXISTS idx_team_model_access_model ON team_model_access(model_id);
+CREATE INDEX IF NOT EXISTS idx_team_permissions_permission ON team_permissions(permission_id);
+
+INSERT INTO teams (name, description, is_default, allow_all_models)
+VALUES ('General', 'Default team for all users', true, true)
+ON CONFLICT (name) DO NOTHING;
+
+-- Budgets and analytics
+DO $$ BEGIN
+ CREATE TYPE budget_kind AS ENUM ('pooled', 'per_user');
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+DO $$ BEGIN
+ CREATE TYPE budget_interval AS ENUM ('daily', 'weekly', 'monthly', 'total');
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+DO $$ BEGIN
+ CREATE TYPE budget_reset_strategy AS ENUM ('calendar', 'rolling', 'anchored');
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+DO $$ BEGIN
+ CREATE TYPE budget_on_exceed AS ENUM ('block', 'warn', 'allow');
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+
+CREATE TABLE IF NOT EXISTS budgets (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name VARCHAR(120) NOT NULL,
+ description TEXT,
+ amount NUMERIC(12,4) NOT NULL CHECK (amount >= 0),
+ kind budget_kind NOT NULL,
+ interval budget_interval NOT NULL,
+ reset_strategy budget_reset_strategy NOT NULL DEFAULT 'calendar',
+ on_exceed budget_on_exceed NOT NULL DEFAULT 'block',
+ is_enabled BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS budget_assignments (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ budget_id UUID NOT NULL REFERENCES budgets(id) ON DELETE CASCADE,
+ team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ CONSTRAINT budget_assignments_target_check CHECK (
+ (team_id IS NOT NULL AND user_id IS NULL) OR
+ (team_id IS NULL AND user_id IS NOT NULL)
+ )
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS budget_assignments_team_budget_unique
+ ON budget_assignments (team_id, budget_id)
+ WHERE team_id IS NOT NULL;
+CREATE UNIQUE INDEX IF NOT EXISTS budget_assignments_user_budget_unique
+ ON budget_assignments (user_id, budget_id)
+ WHERE user_id IS NOT NULL;
+
+CREATE TABLE IF NOT EXISTS usage_events (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
+ model_id UUID REFERENCES models(id) ON DELETE SET NULL,
+ provider_id UUID REFERENCES providers(id) ON DELETE SET NULL,
+ request_type VARCHAR(50) NOT NULL,
+ input_tokens INTEGER NOT NULL DEFAULT 0 CHECK (input_tokens >= 0),
+ output_tokens INTEGER NOT NULL DEFAULT 0 CHECK (output_tokens >= 0),
+ reasoning_tokens INTEGER NOT NULL DEFAULT 0 CHECK (reasoning_tokens >= 0),
+ cost_total NUMERIC(12,6) NOT NULL DEFAULT 0 CHECK (cost_total >= 0),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_usage_events_user_created ON usage_events(user_id, created_at);
+CREATE INDEX IF NOT EXISTS idx_usage_events_model_created ON usage_events(model_id, created_at);
+CREATE INDEX IF NOT EXISTS idx_usage_events_team_created ON usage_events(team_id, created_at);
+
+CREATE TABLE IF NOT EXISTS model_pricing_overrides (
+ model_id UUID PRIMARY KEY REFERENCES models(id) ON DELETE CASCADE,
+ pricing JSONB NOT NULL CHECK (
+ (pricing->>'input') IS NOT NULL AND
+ (pricing->>'output') IS NOT NULL AND
+ (pricing->>'input')::numeric >= 0 AND
+ (pricing->>'output')::numeric >= 0
+ ),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS budget_reset_events (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ assignment_id UUID REFERENCES budget_assignments(id) ON DELETE CASCADE,
+ budget_id UUID REFERENCES budgets(id) ON DELETE CASCADE,
+ team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
+ kind budget_kind,
+ reason TEXT,
+ reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ created_by UUID REFERENCES users(id) ON DELETE SET NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ CONSTRAINT budget_reset_events_scope_check CHECK (
+ num_nonnulls(assignment_id, budget_id, team_id, user_id) = 1
+ )
+);
+
+CREATE INDEX IF NOT EXISTS idx_budget_reset_events_assignment ON budget_reset_events(assignment_id, reset_at);
+CREATE INDEX IF NOT EXISTS idx_budget_reset_events_budget ON budget_reset_events(budget_id, reset_at);
+CREATE INDEX IF NOT EXISTS idx_budget_reset_events_team ON budget_reset_events(team_id, reset_at);
+CREATE INDEX IF NOT EXISTS idx_budget_reset_events_user ON budget_reset_events(user_id, reset_at);
+-- Provider billing
+CREATE TABLE provider_billing_connections (
+ provider_id UUID PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
+ credential TEXT,
+ external_scope_id TEXT,
+ external_scope_name TEXT,
+ is_enabled BOOLEAN NOT NULL DEFAULT false,
+ last_status TEXT NOT NULL DEFAULT 'NOT_CONFIGURED',
+ last_error_code TEXT,
+ last_synced_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ CONSTRAINT provider_billing_status_check CHECK (
+ last_status IN ('NOT_CONFIGURED', 'AVAILABLE', 'UNSUPPORTED', 'UNAUTHORIZED', 'UPSTREAM_ERROR')
+ )
+);
+
+CREATE TABLE provider_billing_snapshots (
+ provider_id UUID PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
+ metric_kind TEXT NOT NULL,
+ currency TEXT NOT NULL,
+ period_start TIMESTAMPTZ,
+ period_end TIMESTAMPTZ,
+ limit_amount NUMERIC,
+ spent_amount NUMERIC,
+ remaining_amount NUMERIC,
+ is_hard_limit BOOLEAN NOT NULL DEFAULT false,
+ thresholds JSONB NOT NULL DEFAULT '[]'::jsonb,
+ details JSONB NOT NULL DEFAULT '{}'::jsonb,
+ fetched_at TIMESTAMPTZ NOT NULL,
+ CONSTRAINT provider_billing_metric_kind_check CHECK (
+ metric_kind IN ('CREDIT_BALANCE', 'KEY_LIMIT', 'SPEND_THRESHOLD', 'SPEND_ONLY')
+ )
+);
-- Indexes for common queries
CREATE INDEX IF NOT EXISTS idx_i18n_translations_language ON i18n_translations(language);
@@ -542,1093 +805,1740 @@ CREATE INDEX IF NOT EXISTS idx_tool_functions_tool ON tool_functions(tool_id);
CREATE INDEX IF NOT EXISTS idx_tool_functions_name ON tool_functions(tool_id, name);
---- Translations
+-- Translations
INSERT INTO i18n_translations (language, key_path, value) VALUES
- -- Common
- ('en', 'common.copy_to_clipboard', 'Copied to clipboard'),
- ('de', 'common.copy_to_clipboard', 'In die Zwischenablage kopiert'),
- ('en', 'common.create', 'Create'),
- ('de', 'common.create', 'Erstellen'),
- ('en', 'common.next', 'Next'),
- ('de', 'common.next', 'Nächste'),
- ('en', 'common.previous', 'Previous'),
- ('de', 'common.previous', 'Vorherige'),
- ('en', 'common.error', 'Error'),
- ('en', 'common.loading', 'Loading...'),
- ('en', 'common.user', 'User'),
- ('de', 'common.error', 'Fehler'),
- ('de', 'common.loading', 'Wird geladen...'),
- ('de', 'common.user', 'Benutzer'),
- ('en', 'common.cancel', 'Cancel'),
- ('en', 'common.save', 'Save'),
- ('en', 'common.delete', 'Delete'),
- ('de', 'common.cancel', 'Abbrechen'),
- ('de', 'common.save', 'Speichern'),
- ('de', 'common.delete', 'Löschen'),
- ('en', 'common.close', 'Close'),
- ('de', 'common.close', 'Schließen'),
-
- -- Auth errors
- ('en', 'auth.errors.setup_completed', 'Setup has already been completed'),
- ('en', 'auth.errors.setup_required', 'Setup must be completed first'),
- ('en', 'auth.errors.invalid_email', 'Please enter a valid email address'),
- ('en', 'auth.errors.username_too_short', 'Username must be at least 3 characters'),
- ('en', 'auth.errors.username_too_long', 'Username must be at most 32 characters'),
- ('en', 'auth.errors.username_invalid_chars', 'Username can only contain letters, numbers, underscores, and hyphens'),
- ('en', 'auth.errors.password_too_short', 'Password must be at least 8 characters'),
- ('en', 'auth.errors.email_or_username_taken', 'Email or username already taken'),
- ('en', 'auth.errors.invalid_credentials', 'Invalid email or password'),
- ('en', 'auth.errors.external_auth', 'This account uses external authentication'),
- ('en', 'auth.errors.not_authenticated', 'Not authenticated'),
- ('en', 'auth.errors.internal_error', 'An error occurred. Please try again.'),
- ('en', 'auth.messages.logout_success', 'Logged out successfully'),
- ('en', 'auth.errors.oauth_email_conflict', 'An account with this email already exists. Please log in with your original method.'),
- ('en', 'auth.errors.oauth_email_conflict_title', 'Account Already Exists'),
- ('en', 'auth.errors.oauth_email_conflict_help', 'Your OAuth provider returned an email that is already associated with an existing account. To link your OAuth identity, please log in with your existing credentials first, then link the OAuth provider from your account settings.'),
- ('en', 'auth.errors.oauth_state_mismatch', 'OAuth authentication failed due to a state mismatch. Please try again.'),
- ('en', 'auth.errors.oauth_state_mismatch_title', 'Authentication Failed'),
- ('en', 'auth.errors.oauth_token_error', 'Failed to obtain authentication token. Please try again.'),
- ('en', 'auth.errors.oauth_token_error_title', 'Token Error'),
- ('en', 'auth.errors.oauth_user_info_error', 'Failed to retrieve user information from the OAuth provider.'),
- ('en', 'auth.errors.oauth_user_info_error_title', 'User Info Error'),
- ('en', 'auth.errors.unknown_error', 'An unexpected error occurred. Please try again.'),
- ('en', 'auth.errors.unknown_error_title', 'Error'),
- ('en', 'auth.errors.back_to_login', 'Back to Login'),
- ('en', 'auth.errors.go_home', 'Go Home'),
- ('en', 'auth.errors.email_taken', 'This email is already registered'),
- ('en', 'auth.errors.username_taken', 'This username is already taken'),
-
- ('de', 'auth.errors.setup_completed', 'Die Einrichtung wurde bereits abgeschlossen'),
- ('de', 'auth.errors.setup_required', 'Die Einrichtung muss zuerst abgeschlossen werden'),
- ('de', 'auth.errors.invalid_email', 'Bitte geben Sie eine gültige E-Mail-Adresse ein'),
- ('de', 'auth.errors.username_too_short', 'Der Benutzername muss mindestens 3 Zeichen haben'),
- ('de', 'auth.errors.username_too_long', 'Der Benutzername darf maximal 32 Zeichen haben'),
- ('de', 'auth.errors.username_invalid_chars', 'Der Benutzername darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten'),
- ('de', 'auth.errors.password_too_short', 'Das Passwort muss mindestens 8 Zeichen haben'),
+ ('de', 'admin.tools.mcp.allow_stdio', 'Serverseitige stdio-MCP-Server erlauben'),
+ ('de', 'admin.tools.mcp.allow_stdio_hint', 'Wenn aktiviert, führen globale stdio-MCP-Server beliebige Befehle auf dem OxideChat-Host aus. Nur auf vertrauenswürdigen Einzelinstanzen aktivieren.'),
+ ('de', 'auth.errors.back_to_login', 'Zurück zur Anmeldung'),
('de', 'auth.errors.email_or_username_taken', 'E-Mail oder Benutzername bereits vergeben'),
- ('de', 'auth.errors.invalid_credentials', 'Ungültige E-Mail oder Passwort'),
+ ('de', 'auth.errors.email_taken', 'Diese E-Mail-Adresse ist bereits registriert'),
('de', 'auth.errors.external_auth', 'Dieses Konto verwendet externe Authentifizierung'),
- ('de', 'auth.errors.not_authenticated', 'Nicht authentifiziert'),
+ ('de', 'auth.errors.go_home', 'Zur Startseite'),
('de', 'auth.errors.internal_error', 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'),
- ('de', 'auth.messages.logout_success', 'Erfolgreich abgemeldet'),
+ ('de', 'auth.errors.invalid_credentials', 'Ungültige E-Mail oder Passwort'),
+ ('de', 'auth.errors.invalid_email', 'Bitte geben Sie eine gültige E-Mail-Adresse ein'),
+ ('de', 'auth.errors.not_authenticated', 'Nicht authentifiziert'),
('de', 'auth.errors.oauth_email_conflict', 'Ein Konto mit dieser E-Mail-Adresse existiert bereits. Bitte melden Sie sich mit Ihrer ursprünglichen Methode an.'),
- ('de', 'auth.errors.oauth_email_conflict_title', 'Konto existiert bereits'),
('de', 'auth.errors.oauth_email_conflict_help', 'Ihr OAuth-Anbieter hat eine E-Mail-Adresse zurückgegeben, die bereits mit einem bestehenden Konto verknüpft ist. Um Ihre OAuth-Identität zu verknüpfen, melden Sie sich bitte zuerst mit Ihren bestehenden Anmeldedaten an und verknüpfen Sie dann den OAuth-Anbieter in Ihren Kontoeinstellungen.'),
+ ('de', 'auth.errors.oauth_email_conflict_title', 'Konto existiert bereits'),
('de', 'auth.errors.oauth_state_mismatch', 'OAuth-Authentifizierung aufgrund einer Statusabweichung fehlgeschlagen. Bitte versuchen Sie es erneut.'),
('de', 'auth.errors.oauth_state_mismatch_title', 'Authentifizierung fehlgeschlagen'),
('de', 'auth.errors.oauth_token_error', 'Authentifizierungstoken konnte nicht abgerufen werden. Bitte versuchen Sie es erneut.'),
('de', 'auth.errors.oauth_token_error_title', 'Token-Fehler'),
('de', 'auth.errors.oauth_user_info_error', 'Benutzerinformationen konnten nicht vom OAuth-Anbieter abgerufen werden.'),
('de', 'auth.errors.oauth_user_info_error_title', 'Benutzerinfo-Fehler'),
+ ('de', 'auth.errors.password_no_digit', 'Das Passwort muss mindestens eine Zahl enthalten'),
+ ('de', 'auth.errors.password_no_lowercase', 'Das Passwort muss mindestens einen Kleinbuchstaben enthalten'),
+ ('de', 'auth.errors.password_no_special', 'Das Passwort muss mindestens ein Sonderzeichen enthalten'),
+ ('de', 'auth.errors.password_no_uppercase', 'Das Passwort muss mindestens einen Großbuchstaben enthalten'),
+ ('de', 'auth.errors.password_requirements_not_met', 'Bitte stellen Sie sicher, dass Ihr Passwort alle Anforderungen erfüllt'),
+ ('de', 'auth.errors.password_too_long', 'Das Passwort darf maximal 128 Zeichen haben'),
+ ('de', 'auth.errors.password_too_short', 'Das Passwort muss mindestens 8 Zeichen haben'),
+ ('de', 'auth.errors.setup_completed', 'Die Einrichtung wurde bereits abgeschlossen'),
+ ('de', 'auth.errors.setup_required', 'Die Einrichtung muss zuerst abgeschlossen werden'),
('de', 'auth.errors.unknown_error', 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'),
('de', 'auth.errors.unknown_error_title', 'Fehler'),
- ('de', 'auth.errors.back_to_login', 'Zurück zur Anmeldung'),
- ('de', 'auth.errors.go_home', 'Zur Startseite'),
- ('de', 'auth.errors.email_taken', 'Diese E-Mail-Adresse ist bereits registriert'),
+ ('de', 'auth.errors.username_invalid_chars', 'Der Benutzername darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten'),
('de', 'auth.errors.username_taken', 'Dieser Benutzername ist bereits vergeben'),
-
- -- Sidebar
- ('en', 'sidebar.chats', 'Chats'),
- ('en', 'sidebar.new_chat', 'New Chat'),
- ('en', 'sidebar.logout', 'Log out'),
- ('en', 'sidebar.ai_chat_app', 'AI Chat Application'),
- ('en', 'sidebar.workspace', 'Workspace'),
- ('en', 'sidebar.all_chats', 'All chats'),
- ('en', 'sidebar.all', 'All'),
- ('de', 'sidebar.chats', 'Chats'),
- ('de', 'sidebar.new_chat', 'Neuer Chat'),
- ('de', 'sidebar.logout', 'Abmelden'),
- ('de', 'sidebar.ai_chat_app', 'KI-Chat-Anwendung'),
- ('de', 'sidebar.workspace', 'Arbeitsbereich'),
- ('de', 'sidebar.all_chats', 'Alle Chats'),
- ('de', 'sidebar.all', 'Alle'),
-
- -- Auth: Login
- ('en', 'auth.login.title', 'Welcome back'),
- ('en', 'auth.login.description', 'Login with your favorite external provider'),
- ('en', 'auth.login.google', 'Login with Google'),
- ('en', 'auth.login.discord', 'Login with Discord'),
- ('en', 'auth.login.or_continue', 'Or continue with'),
- ('en', 'auth.login.email', 'Email'),
- ('en', 'auth.login.password', 'Password'),
- ('en', 'auth.login.forgot_password', 'Forgot your password?'),
- ('en', 'auth.login.submit', 'Login'),
- ('en', 'auth.login.submitting', 'Signing in...'),
- ('en', 'auth.login.no_account', 'Don''t have an account?'),
- ('en', 'auth.login.sign_up', 'Sign up'),
- ('en', 'auth.login.terms_prefix', 'By clicking continue, you agree to our'),
- ('en', 'auth.login.terms_link', 'Terms of Service'),
- ('en', 'auth.login.and', 'and'),
- ('en', 'auth.login.privacy_link', 'Privacy Policy'),
-
- ('de', 'auth.login.title', 'Willkommen zurück'),
+ ('de', 'auth.errors.username_too_long', 'Der Benutzername darf maximal 32 Zeichen haben'),
+ ('de', 'auth.errors.username_too_short', 'Der Benutzername muss mindestens 3 Zeichen haben'),
+ ('de', 'auth.login.and', 'und'),
('de', 'auth.login.description', 'Melden Sie sich mit einem externen Provider an'),
- ('de', 'auth.login.google', 'Mit Google anmelden'),
('de', 'auth.login.discord', 'Mit Discord anmelden'),
- ('de', 'auth.login.or_continue', 'Oder weiter mit'),
('de', 'auth.login.email', 'E-Mail'),
- ('de', 'auth.login.password', 'Passwort'),
('de', 'auth.login.forgot_password', 'Passwort vergessen?'),
- ('de', 'auth.login.submit', 'Anmelden'),
- ('de', 'auth.login.submitting', 'Anmeldung...'),
+ ('de', 'auth.login.google', 'Mit Google anmelden'),
('de', 'auth.login.no_account', 'Noch kein Konto?'),
+ ('de', 'auth.login.or_continue', 'Oder weiter mit'),
+ ('de', 'auth.login.password', 'Passwort'),
+ ('de', 'auth.login.privacy_link', 'Datenschutzbestimmungen'),
('de', 'auth.login.sign_up', 'Registrieren'),
- ('de', 'auth.login.terms_prefix', 'Durch Klicken auf "Weiter" erklären Sie sich mit unseren'),
+ ('de', 'auth.login.submit', 'Anmelden'),
+ ('de', 'auth.login.submitting', 'Anmeldung...'),
('de', 'auth.login.terms_link', 'Nutzungsbedingungen'),
- ('de', 'auth.login.and', 'und'),
- ('de', 'auth.login.privacy_link', 'Datenschutzbestimmungen'),
-
- -- Auth: Register
- ('en', 'auth.register.title', 'Create an account'),
- ('en', 'auth.register.description', 'Sign up with your favorite external provider'),
- ('en', 'auth.register.google', 'Sign up with Google'),
- ('en', 'auth.register.discord', 'Sign up with Discord'),
- ('en', 'auth.register.username', 'Username'),
- ('en', 'auth.register.confirm_password', 'Confirm Password'),
- ('en', 'auth.register.submit', 'Create Account'),
- ('en', 'auth.register.submitting', 'Creating account...'),
- ('en', 'auth.register.have_account', 'Already have an account?'),
- ('en', 'auth.register.sign_in', 'Sign in'),
- ('en', 'auth.register.passwords_mismatch', 'Passwords do not match'),
- ('en', 'auth.register.password_requirements', 'At least 8 characters'),
-
- ('de', 'auth.register.title', 'Konto erstellen'),
- ('de', 'auth.register.description', 'Registrieren Sie sich mit einem externen Provider'),
- ('de', 'auth.register.google', 'Mit Google registrieren'),
- ('de', 'auth.register.discord', 'Mit Discord registrieren'),
- ('de', 'auth.register.username', 'Benutzername'),
- ('de', 'auth.register.confirm_password', 'Passwort bestätigen'),
- ('de', 'auth.register.submit', 'Konto erstellen'),
- ('de', 'auth.register.submitting', 'Konto wird erstellt...'),
- ('de', 'auth.register.have_account', 'Haben Sie bereits ein Konto?'),
- ('de', 'auth.register.sign_in', 'Anmelden'),
- ('de', 'auth.register.passwords_mismatch', 'Passwörter stimmen nicht überein'),
- ('de', 'auth.register.password_requirements', 'Mindestens 8 Zeichen'),
-
- -- Auth: Setup
- ('en', 'auth.setup.title', 'Welcome to OxideChat'),
- ('en', 'auth.setup.description', 'No users exist yet. Create your admin account to get started.'),
- ('en', 'auth.setup.submit', 'Create Admin Account'),
- ('en', 'auth.setup.one_time_tip', 'This is a one-time setup. The first account will have administrator privileges.'),
-
- ('de', 'auth.setup.title', 'Willkommen bei OxideChat'),
- ('de', 'auth.setup.description', 'Es existieren noch keine Benutzer. Erstellen Sie Ihr Admin-Konto, um zu beginnen.'),
- ('de', 'auth.setup.submit', 'Admin-Konto erstellen'),
- ('de', 'auth.setup.one_time_tip', 'Dies ist eine einmalige Einrichtung. Das erste Konto erhält Administratorrechte.'),
-
- -- Welcome Page
- ('en', 'welcome.title', 'Welcome to OxideChat'),
- ('en', 'welcome.description', 'Select a chat from the sidebar or start a new conversation'),
- ('de', 'welcome.title', 'Willkommen bei OxideChat'),
- ('de', 'welcome.description', 'Wählen Sie einen Chat aus der Seitenleiste oder beginnen Sie eine neue Konversation'),
-
- -- Calendar Component (used in DialogCalendar.vue)
- ('en', 'guild.components.dialog_calendar.date.select', 'Select Date'),
- ('en', 'guild.components.dialog_calendar.time.label', 'Time'),
- ('en', 'guild.components.dialog_calendar.time.select', 'Select Time'),
- ('en', 'guild.components.dialog_calendar.reset', 'Reset'),
- ('en', 'guild.components.dialog_calendar.ok', 'OK'),
-
- ('de', 'guild.components.dialog_calendar.date.select', 'Datum auswählen'),
- ('de', 'guild.components.dialog_calendar.time.label', 'Uhrzeit'),
- ('de', 'guild.components.dialog_calendar.time.select', 'Uhrzeit auswählen'),
- ('de', 'guild.components.dialog_calendar.reset', 'Zurücksetzen'),
- ('de', 'guild.components.dialog_calendar.ok', 'OK'),
-
- ('en', 'auth.errors.password_too_long', 'Password must be at most 128 characters'),
- ('en', 'auth.errors.password_no_uppercase', 'Password must contain at least one uppercase letter'),
- ('en', 'auth.errors.password_no_lowercase', 'Password must contain at least one lowercase letter'),
- ('en', 'auth.errors.password_no_digit', 'Password must contain at least one number'),
- ('en', 'auth.errors.password_no_special', 'Password must contain at least one special character'),
- ('en', 'auth.errors.password_requirements_not_met', 'Please ensure your password meets all requirements'),
-
- -- Password validation error messages (German)
- ('de', 'auth.errors.password_too_long', 'Das Passwort darf maximal 128 Zeichen haben'),
- ('de', 'auth.errors.password_no_uppercase', 'Das Passwort muss mindestens einen Großbuchstaben enthalten'),
- ('de', 'auth.errors.password_no_lowercase', 'Das Passwort muss mindestens einen Kleinbuchstaben enthalten'),
- ('de', 'auth.errors.password_no_digit', 'Das Passwort muss mindestens eine Zahl enthalten'),
- ('de', 'auth.errors.password_no_special', 'Das Passwort muss mindestens ein Sonderzeichen enthalten'),
- ('de', 'auth.errors.password_requirements_not_met', 'Bitte stellen Sie sicher, dass Ihr Passwort alle Anforderungen erfüllt'),
-
- ('en', 'error_messages.theme_import_failed', 'Failed to import theme'),
- ('de', 'error_messages.theme_import_failed', 'Fehler beim Importieren des Themes'),
-
- -- Password strength indicator labels (English)
- ('en', 'auth.password.min_length', 'At least {min} characters'),
- ('en', 'auth.password.max_length', 'Maximum {max} characters'),
- ('en', 'auth.password.uppercase', 'One uppercase letter'),
- ('en', 'auth.password.lowercase', 'One lowercase letter'),
- ('en', 'auth.password.digit', 'One number'),
- ('en', 'auth.password.special', 'One special character'),
- ('en', 'auth.password.strength_weak', 'Weak'),
- ('en', 'auth.password.strength_fair', 'Fair'),
- ('en', 'auth.password.strength_good', 'Good'),
-
- -- Password strength indicator labels (German)
- ('de', 'auth.password.min_length', 'Mindestens {min} Zeichen'),
- ('de', 'auth.password.max_length', 'Maximal {max} Zeichen'),
- ('de', 'auth.password.uppercase', 'Ein Großbuchstabe'),
- ('de', 'auth.password.lowercase', 'Ein Kleinbuchstabe'),
- ('de', 'auth.password.digit', 'Eine Zahl'),
- ('de', 'auth.password.special', 'Ein Sonderzeichen'),
- ('de', 'auth.password.strength_weak', 'Schwach'),
- ('de', 'auth.password.strength_fair', 'Mittel'),
- ('de', 'auth.password.strength_good', 'Gut'),
-
- ('en', 'settings.title', 'Settings'),
- ('en', 'settings.description', 'Manage your account preferences and configuration.'),
- ('en', 'settings.back', 'Back'),
- ('en', 'settings.tabs.profile', 'Profile'),
- ('en', 'settings.tabs.providers', 'Providers'),
- ('en', 'settings.tabs.models', 'Models'),
- ('en', 'settings.tabs.ai_options', 'AI Options'),
- ('en', 'settings.tabs.usage_analytics', 'Usage Analytics'),
- ('en', 'settings.tabs.admin_users', 'Users'),
- ('en', 'settings.tabs.admin_config', 'Configuration'),
- ('en', 'settings.tabs.appearance', 'Appearance'),
- ('en', 'settings.admin_config.description', 'Application configuration settings'),
- ('en', 'settings.admin_config.coming_soon', 'Configuration management coming soon'),
- ('en', 'settings.admin_users.description', 'Manage user accounts and permissions'),
- ('en', 'settings.admin_users.coming_soon', 'User management coming soon'),
- ('en', 'settings.profile.title', 'Profile Information'),
- ('en', 'settings.profile.description', 'Your personal account information'),
- ('en', 'settings.profile.name', 'Name'),
- ('en', 'settings.profile.email', 'Email'),
- ('en', 'settings.profile.user_id', 'User ID'),
- ('en', 'settings.profile.edit', 'Edit'),
- ('en', 'settings.sessions.title', 'Active Sessions'),
- ('en', 'settings.sessions.description', 'Manage your active login sessions across devices'),
- ('en', 'settings.sessions.current', 'Current'),
- ('en', 'settings.sessions.sign_out', 'Sign Out'),
- ('en', 'settings.sessions.last_seen', 'Last seen'),
- ('en', 'settings.appearance.title', 'Appearance'),
- ('en', 'settings.appearance.description', 'Customize the look and feel'),
- ('en', 'settings.appearance.theme', 'Theme'),
- ('en', 'settings.appearance.theme_description', 'Choose between light and dark mode'),
- ('en', 'settings.appearance.theme_light', 'Light'),
- ('en', 'settings.appearance.theme_dark', 'Dark'),
- ('en', 'settings.appearance.theme_system', 'System'),
- ('en', 'settings.appearance.language', 'Language'),
- ('en', 'settings.appearance.language_description', 'Select your preferred language'),
- ('en', 'settings.appearance.language_en', 'English'),
- ('en', 'settings.appearance.language_de', 'German'),
- ('en', 'settings.appearance.themes', 'Themes'),
- ('en', 'settings.appearance.themes_description', 'Choose from a variety of beautiful themes'),
- ('en', 'settings.appearance.display_mode', 'Display Mode'),
- ('en', 'settings.appearance.display_mode_description', 'Select how content appears'),
- ('en', 'settings.appearance.built_in_themes', 'Built-in Themes'),
- ('en', 'settings.appearance.custom_themes', 'Custom Themes'),
- ('en', 'settings.appearance.no_themes_found', 'No themes found'),
- ('en', 'settings.appearance.try_different_search', 'Try a different search term'),
- ('en', 'settings.appearance.themes_powered_by', 'Themes powered by'),
- ('en', 'settings.appearance.search_themes', 'Search themes...'),
- ('en', 'settings.appearance.random_theme', 'Random theme'),
- ('en', 'settings.appearance.reset_theme', 'Reset theme'),
- ('en', 'settings.appearance.import_theme', 'Import Theme'),
- ('en', 'settings.appearance.loading_themes', 'Loading themes'),
- ('en', 'settings.theme_imported', 'Theme imported successfully'),
- ('en', 'settings.theme_deleted', 'Theme deleted'),
- ('en', 'settings.theme_saved', 'Theme preferences saved'),
- ('en', 'settings.import_theme', 'Import Theme'),
- ('en', 'settings.import_theme_description', 'Paste a theme URL from'),
- ('en', 'settings.theme_url', 'Theme URL'),
- ('en', 'settings.import', 'Import'),
-
- ('de', 'settings.appearance.themes', 'Themes'),
- ('de', 'settings.appearance.themes_description', 'Wählen Sie aus einer Vielzahl von schönen Themes'),
- ('de', 'settings.appearance.display_mode', 'Anzeigemodus'),
- ('de', 'settings.appearance.display_mode_description', 'Wählen Sie, wie der Inhalt angezeigt wird'),
- ('de', 'settings.appearance.built_in_themes', 'Integrierte Themes'),
- ('de', 'settings.appearance.custom_themes', 'Benutzerdefinierte Themes'),
- ('de', 'settings.appearance.no_themes_found', 'Keine Themes gefunden'),
- ('de', 'settings.appearance.try_different_search', 'Versuchen Sie einen anderen Suchbegriff'),
- ('de', 'settings.appearance.themes_powered_by', 'Themes bereitgestellt von'),
- ('de', 'settings.appearance.search_themes', 'Themes durchsuchen...'),
- ('de', 'settings.appearance.random_theme', 'Zufalliges Theme'),
- ('de', 'settings.appearance.reset_theme', 'Theme zurücksetzen'),
- ('de', 'settings.appearance.import_theme', 'Theme importieren'),
- ('de', 'settings.appearance.loading_themes', 'Themes werden geladen'),
- ('de', 'settings.theme_imported', 'Theme erfolgreich importiert'),
- ('de', 'settings.theme_deleted', 'Theme gelöscht'),
- ('de', 'settings.theme_saved', 'Theme-Einstellungen gespeichert'),
- ('de', 'settings.import_theme', 'Theme importieren'),
- ('de', 'settings.import_theme_description', 'Fügen Sie eine Theme-URL von'),
- ('de', 'settings.theme_url', 'Theme-URL'),
- ('de', 'settings.import', 'Importieren'),
-
- ('de', 'settings.title', 'Einstellungen'),
- ('de', 'settings.description', 'Verwalten Sie Ihre Kontoeinstellungen und Konfiguration.'),
- ('de', 'settings.back', 'Zurück'),
- ('de', 'settings.tabs.profile', 'Profil'),
- ('de', 'settings.tabs.providers', 'Anbieter'),
- ('de', 'settings.tabs.models', 'Modelle'),
- ('de', 'settings.tabs.ai_options', 'AI Optionen'),
- ('de', 'settings.tabs.usage_analytics', 'Benutzungsstatistiken'),
- ('de', 'settings.tabs.admin_users', 'Benutzer'),
- ('de', 'settings.tabs.admin_config', 'Konfiguration'),
- ('de', 'settings.tabs.appearance', 'Erscheinungsbild'),
- ('de', 'settings.admin_config.description', 'Anwendungskonfigurationseinstellungen'),
- ('de', 'settings.admin_config.coming_soon', 'Konfigurationsverwaltung kommt bald'),
- ('de', 'settings.admin_users.description', 'Benutzerkonten und Berechtigungen verwalten'),
- ('de', 'settings.admin_users.coming_soon', 'Benutzerverwaltung kommt bald'),
- ('de', 'settings.profile.title', 'Profilinformationen'),
- ('de', 'settings.profile.description', 'Ihre persönlichen Kontoinformationen'),
- ('de', 'settings.profile.name', 'Name'),
- ('de', 'settings.profile.email', 'E-Mail'),
- ('de', 'settings.profile.user_id', 'Benutzer-ID'),
- ('de', 'settings.profile.edit', 'Bearbeiten'),
- ('de', 'settings.sessions.title', 'Aktive Sitzungen'),
- ('de', 'settings.sessions.description', 'Verwalten Sie Ihre aktiven Anmeldesitzungen'),
- ('de', 'settings.sessions.current', 'Aktuell'),
- ('de', 'settings.sessions.sign_out', 'Abmelden'),
- ('de', 'settings.sessions.last_seen', 'Zuletzt gesehen'),
- ('de', 'settings.appearance.title', 'Erscheinungsbild'),
- ('de', 'settings.appearance.description', 'Passen Sie das Aussehen an'),
- ('de', 'settings.appearance.theme', 'Theme'),
- ('de', 'settings.appearance.theme_description', 'Wählen Sie zwischen Hell- und Dunkelmodus'),
- ('de', 'settings.appearance.theme_light', 'Hell'),
- ('de', 'settings.appearance.theme_dark', 'Dunkel'),
- ('de', 'settings.appearance.theme_system', 'System'),
- ('de', 'settings.appearance.language', 'Sprache'),
- ('de', 'settings.appearance.language_description', 'Wählen Sie Ihre bevorzugte Sprache'),
- ('de', 'settings.appearance.language_en', 'Englisch'),
- ('de', 'settings.appearance.language_de', 'Deutsch'),
-
- -- Providers
- ('en', 'settings.providers.title', 'BYOK AI Providers'),
- ('en', 'settings.providers.description', 'Bring your own API keys for enhanced AI capabilities'),
- ('en', 'settings.providers.edit', 'Configure'),
- ('en', 'settings.providers.configure', 'Configure'),
- ('en', 'settings.providers.configure_description', 'Enter your API credentials to enable this provider'),
- ('en', 'settings.providers.configured', 'Configured'),
- ('en', 'settings.providers.add_custom', 'Add Custom Provider'),
- ('en', 'settings.providers.name', 'Display Name'),
- ('en', 'settings.providers.api_key', 'API Key'),
- ('en', 'settings.providers.api_key_placeholder', 'Enter your API key'),
- ('en', 'settings.providers.api_key_hint', 'Your API key is stored securely. If encryption is configured, it is encrypted at rest'),
- ('en', 'settings.providers.base_url', 'Base URL'),
- ('en', 'settings.providers.enabled', 'Enable Provider'),
- ('en', 'settings.providers.enabled_hint', 'Allow this provider to be used for AI requests'),
- ('en', 'settings.providers.save_success', 'Provider configuration saved successfully'),
- ('en', 'settings.providers.save_error', 'Failed to save provider configuration'),
- ('en', 'settings.providers.delete_success', 'Provider removed successfully'),
- ('en', 'settings.providers.delete_error', 'Failed to remove provider'),
- ('en', 'settings.providers.openrouter_description', 'Access a wide variety of models through OpenRouter'),
- ('en', 'settings.providers.openai_description', 'Access GPT-5.2, GPT-5.2-Codex and other OpenAI models'),
- ('en', 'settings.providers.anthropic_description', 'Access Opus 4.5, Sonnet 4.5 and other Anthropic models'),
- ('en', 'settings.providers.google_description', 'Access Gemini 3 family and other Google AI models'),
- ('en', 'settings.providers.toggling_provider', 'Toggling provider'),
- ('en', 'settings.providers.toggling_provider_description', 'Please wait while the provider is being toggled'),
- ('en', 'settings.providers.toggling_provider_success', 'Provider successfully toggled'),
- ('en', 'settings.providers.syncing_provider', 'Syncing models'),
- ('en', 'settings.providers.syncing_provider_description', 'Models for this provider are being synced, this may take a moment'),
- ('en', 'settings.providers.syncing_provider_success', 'Models successfully synced'),
-
- ('de', 'settings.providers.title', 'BYOK AI-Anbieter'),
- ('de', 'settings.providers.description', 'Verwenden Sie Ihre eigenen API-Schlüssel für erweiterte AI-Funktionen'),
- ('de', 'settings.providers.edit', 'Konfigurieren'),
- ('de', 'settings.providers.configure', 'Konfigurieren'),
- ('de', 'settings.providers.configure_description', 'Geben Sie Ihre API-Anmeldedaten ein, um diesen Anbieter zu aktivieren'),
- ('de', 'settings.providers.configured', 'Konfiguriert'),
- ('de', 'settings.providers.add_custom', 'Benutzerdefinierten Anbieter hinzufügen'),
- ('de', 'settings.providers.name', 'Anzeigename'),
- ('de', 'settings.providers.api_key', 'API-Schlüssel'),
- ('de', 'settings.providers.api_key_placeholder', 'Geben Sie Ihren API-Schlüssel ein'),
- ('de', 'settings.providers.api_key_hint', 'Ihr API-Schlüssel wird sicher gespeichert. Wenn zudem die Verschlüsselung konfiguriert ist, wird er zudem verschlüsselt'),
- ('de', 'settings.providers.base_url', 'Basis-URL'),
- ('de', 'settings.providers.enabled', 'Anbieter aktivieren'),
- ('de', 'settings.providers.enabled_hint', 'Diesen Anbieter für AI-Anfragen verwenden'),
- ('de', 'settings.providers.save_success', 'Anbieterkonfiguration erfolgreich gespeichert'),
- ('de', 'settings.providers.save_error', 'Fehler beim Speichern der Anbieterkonfiguration'),
- ('de', 'settings.providers.delete_success', 'Anbieter erfolgreich entfernt'),
- ('de', 'settings.providers.delete_error', 'Fehler beim Entfernen des Anbieters'),
- ('de', 'settings.providers.openrouter_description', 'Greift auf eine Vielzahl von Modellen über OpenRouter zu'),
- ('de', 'settings.providers.openai_description', 'Greift auf GPT-5.2, GPT-5.2-Codex und andere OpenAI-Modelle zu'),
- ('de', 'settings.providers.anthropic_description', 'Greift auf Opus 4.5, Sonnet 4.5 und andere Anthropic-Modelle zu'),
- ('de', 'settings.providers.google_description', 'Greift auf Gemini 3 Familie und andere Google-Modelle zu'),
- ('de', 'settings.providers.toggling_provider', 'Anbieter umschalten'),
- ('de', 'settings.providers.toggling_provider_description', 'Bitte warten Sie, während der Anbieter umgeschaltet wird'),
- ('de', 'settings.providers.toggling_provider_success', 'Anbieter erfolgreich umgeschaltet'),
- ('de', 'settings.providers.syncing_provider', 'Synchronisiere Modelle'),
- ('de', 'settings.providers.syncing_provider_description', 'Modelle des Anbieters werden synchronisiert, dies kann einen Moment dauern'),
- ('de', 'settings.providers.syncing_provider_success', 'Modelle erfolgreich synchronisiert'),
-
- -- Chat Composer
- ('en', 'chat.composer.placeholder_default', 'Select a model and start typing...'),
- ('en', 'chat.composer.placeholder_model', 'Message {model}...'),
- ('en', 'chat.composer.hint', 'Press Enter to send, Shift+Enter for new line'),
-
- -- Chat Context Menu
- ('en', 'chat.context_menu.pin', 'Pin'),
- ('en', 'chat.context_menu.unpin', 'Unpin'),
- ('en', 'chat.context_menu.rename', 'Rename'),
- ('en', 'chat.context_menu.rename_prompt', 'Enter new title:'),
- ('en', 'chat.context_menu.move_to', 'Move to...'),
- ('en', 'chat.context_menu.archive', 'Archive'),
- ('en', 'chat.context_menu.unarchive', 'Unarchive'),
- ('en', 'chat.context_menu.export', 'Export'),
- ('en', 'chat.context_menu.delete', 'Delete'),
- ('en', 'chat.context_menu.delete_confirm', 'Are you sure you want to delete this chat? This cannot be undone.'),
-
- -- Chat List
- ('en', 'chat.list.new_chat', 'New chat'),
- ('en', 'chat.list.pinned', 'Pinned'),
- ('en', 'chat.list.today', 'Today'),
- ('en', 'chat.list.yesterday', 'Yesterday'),
- ('en', 'chat.list.past_7_days', 'Past 7 days'),
- ('en', 'chat.list.past_30_days', 'Past 30 days'),
- ('en', 'chat.list.older', 'Older'),
-
- -- Chat Empty State
- ('en', 'chat.empty_state.username_default', 'there'),
- ('en', 'chat.empty_state.greeting_morning', 'Good morning'),
- ('en', 'chat.empty_state.greeting_afternoon', 'Good afternoon'),
- ('en', 'chat.empty_state.greeting_evening', 'Good evening'),
- ('en', 'chat.empty_state.desc_1', 'Ask me anything about code, math, or creative writing.'),
- ('en', 'chat.empty_state.desc_2', 'I can help you brainstorm, analyze data, or write content.'),
- ('en', 'chat.empty_state.desc_3', 'Need help with a project? Just describe what you''re working on.'),
- ('en', 'chat.empty_state.desc_4', 'I''m here to assist with research, explanations, or problem-solving.'),
- ('en', 'chat.empty_state.desc_5', 'Start a conversation and let''s explore ideas together.'),
-
- -- Code Preview
- ('en', 'chat.code_preview.title', 'Preview:'),
- ('en', 'chat.code_preview.preview', 'Preview'),
- ('en', 'chat.code_preview.code', 'Code'),
- ('en', 'chat.code_preview.copied', 'Copied'),
- ('en', 'chat.code_preview.copy', 'Copy'),
- ('en', 'chat.code_preview.open_in_tab', 'Open in Tab'),
- ('en', 'chat.code_preview.close', 'Close'),
- ('en', 'chat.code_preview.lines', '{count} lines'),
-
- -- Context Indicator
- ('en', 'chat.context_indicator.usage', 'Context Usage'),
- ('en', 'chat.context_indicator.tokens', 'tokens'),
- ('en', 'chat.context_indicator.approaching_limit', 'Approaching context limit'),
-
- -- Message Actions
- ('en', 'chat.message_actions.copied', 'Copied!'),
- ('en', 'chat.message_actions.copy_message', 'Copy message'),
- ('en', 'chat.message_actions.regenerate', 'Regenerate response'),
- ('en', 'chat.message_actions.details', 'Message Details'),
- ('en', 'chat.message_actions.model', 'Model'),
- ('en', 'chat.message_actions.input_tokens', 'Input tokens'),
- ('en', 'chat.message_actions.output_tokens', 'Output tokens'),
- ('en', 'chat.message_actions.reasoning_tokens', 'Reasoning tokens'),
- ('en', 'chat.message_actions.input_cost', 'Input cost'),
- ('en', 'chat.message_actions.output_cost', 'Output cost'),
- ('en', 'chat.message_actions.reasoning_cost', 'Reasoning cost'),
- ('en', 'chat.message_actions.total_cost', 'Total cost'),
- ('en', 'chat.message_actions.response_latency', 'Response latency'),
- ('en', 'chat.message_actions.reasoning_latency', 'Reasoning latency'),
- ('en', 'chat.message_actions.created', 'Created'),
- ('en', 'chat.message_actions.branch_to_new_chat', 'Branch to new chat'),
-
- -- Model Selector
- ('en', 'chat.model_selector.search_models', 'Search models...'),
- ('en', 'chat.model_selector.select_model', 'Select model'),
- ('en', 'chat.model_selector.no_models', 'No models found'),
- ('en', 'chat.model_selector.tool_calling', 'Tool calling'),
- ('en', 'chat.model_selector.reasoning_capable', 'Reasoning capable'),
- ('en', 'chat.model_selector.toggle_favorite', 'Toggle favorite'),
- ('en', 'chat.model_selector.vision_capable', 'Vision capable'),
-
- -- Reasoning Selector
- ('en', 'chat.reasoning_selector.disabled', 'Disabled'),
- ('en', 'chat.reasoning_selector.token_limit', 'Token Limit'),
- ('en', 'chat.reasoning_selector.tokens', 'Tokens'),
- ('en', 'chat.reasoning_selector.none', 'None'),
- ('en', 'chat.reasoning_selector.minimal', 'Minimal'),
- ('en', 'chat.reasoning_selector.low', 'Low'),
- ('en', 'chat.reasoning_selector.medium', 'Medium'),
- ('en', 'chat.reasoning_selector.high', 'High'),
- ('en', 'chat.reasoning_selector.extra_high', 'Extra High'),
-
- -- Tool Selector
- ('en', 'chat.tool_selector.search_tools', 'Search tools...'),
- ('en', 'chat.tool_selector.no_tool_support', 'Selected model doesn''t support tools'),
- ('en', 'chat.tool_selector.no_tools_found', 'No tools found'),
-
- -- Streaming Message
- ('en', 'chat.streaming_message.assistant', 'Assistant'),
-
- -- German translations
- -- Chat Composer
+ ('de', 'auth.login.terms_prefix', 'Durch Klicken auf "Weiter" erklären Sie sich mit unseren'),
+ ('de', 'auth.login.title', 'Willkommen zurück'),
+ ('de', 'auth.messages.logout_success', 'Erfolgreich abgemeldet'),
+ ('de', 'auth.password.digit', 'Eine Zahl'),
+ ('de', 'auth.password.lowercase', 'Ein Kleinbuchstabe'),
+ ('de', 'auth.password.max_length', 'Maximal {max} Zeichen'),
+ ('de', 'auth.password.min_length', 'Mindestens {min} Zeichen'),
+ ('de', 'auth.password.special', 'Ein Sonderzeichen'),
+ ('de', 'auth.password.strength_fair', 'Mittel'),
+ ('de', 'auth.password.strength_good', 'Gut'),
+ ('de', 'auth.password.strength_weak', 'Schwach'),
+ ('de', 'auth.password.uppercase', 'Ein Großbuchstabe'),
+ ('de', 'auth.register.confirm_password', 'Passwort bestätigen'),
+ ('de', 'auth.register.description', 'Registrieren Sie sich mit einem externen Provider'),
+ ('de', 'auth.register.discord', 'Mit Discord registrieren'),
+ ('de', 'auth.register.google', 'Mit Google registrieren'),
+ ('de', 'auth.register.have_account', 'Haben Sie bereits ein Konto?'),
+ ('de', 'auth.register.password_requirements', 'Mindestens 8 Zeichen'),
+ ('de', 'auth.register.passwords_mismatch', 'Passwörter stimmen nicht überein'),
+ ('de', 'auth.register.sign_in', 'Anmelden'),
+ ('de', 'auth.register.submit', 'Konto erstellen'),
+ ('de', 'auth.register.submitting', 'Konto wird erstellt...'),
+ ('de', 'auth.register.title', 'Konto erstellen'),
+ ('de', 'auth.register.username', 'Benutzername'),
+ ('de', 'auth.setup.description', 'Es existieren noch keine Benutzer. Erstellen Sie Ihr Admin-Konto, um zu beginnen.'),
+ ('de', 'auth.setup.one_time_tip', 'Dies ist eine einmalige Einrichtung. Das erste Konto erhält Administratorrechte.'),
+ ('de', 'auth.setup.submit', 'Admin-Konto erstellen'),
+ ('de', 'auth.setup.title', 'Willkommen bei OxideChat'),
+ ('de', 'chat.code_preview.close', 'Schließen'),
+ ('de', 'chat.code_preview.code', 'Code'),
+ ('de', 'chat.code_preview.copied', 'Kopiert!'),
+ ('de', 'chat.code_preview.copy', 'Kopieren'),
+ ('de', 'chat.code_preview.lines', '{count} Zeilen'),
+ ('de', 'chat.code_preview.open_in_tab', 'In Tab öffnen'),
+ ('de', 'chat.code_preview.preview', 'Vorschau'),
+ ('de', 'chat.code_preview.title', 'Vorschau:'),
+ ('de', 'chat.composer.hint', 'Drücken Sie Enter zum Senden, Shift+Enter für eine neue Zeile'),
('de', 'chat.composer.placeholder_default', 'Wählen Sie ein Modell und beginnen Sie zu tippen...'),
('de', 'chat.composer.placeholder_model', 'Nachricht an {model}...'),
- ('de', 'chat.composer.hint', 'Drücken Sie Enter zum Senden, Shift+Enter für eine neue Zeile'),
-
- -- Chat Context Menu
+ ('de', 'chat.context_indicator.approaching_limit', 'Kontextlimit nähert sich'),
+ ('de', 'chat.context_indicator.tokens', 'Tokens'),
+ ('de', 'chat.context_indicator.usage', 'Kontextnutzung'),
+ ('de', 'chat.context_menu.archive', 'Archivieren'),
+ ('de', 'chat.context_menu.delete', 'Löschen'),
+ ('de', 'chat.context_menu.delete_confirm', 'Möchten Sie diesen Chat wirklich löschen? Dies kann nicht rückgängig gemacht werden.'),
+ ('de', 'chat.context_menu.export', 'Exportieren'),
+ ('de', 'chat.context_menu.move_to', 'Verschieben nach...'),
+ ('de', 'chat.context_menu.no_workspace', 'Kein Arbeitsbereich'),
('de', 'chat.context_menu.pin', 'Anheften'),
- ('de', 'chat.context_menu.unpin', 'Lösen'),
('de', 'chat.context_menu.rename', 'Umbenennen'),
('de', 'chat.context_menu.rename_prompt', 'Neuen Titel eingeben:'),
- ('de', 'chat.context_menu.move_to', 'Verschieben nach...'),
- ('de', 'chat.context_menu.archive', 'Archivieren'),
('de', 'chat.context_menu.unarchive', 'Aus Archiv entnehmen'),
- ('de', 'chat.context_menu.export', 'Exportieren'),
- ('de', 'chat.context_menu.delete', 'Löschen'),
- ('de', 'chat.context_menu.delete_confirm', 'Möchten Sie diesen Chat wirklich löschen? Dies kann nicht rückgängig gemacht werden.'),
-
- -- Chat List
- ('de', 'chat.list.new_chat', 'Neuer Chat'),
- ('de', 'chat.list.pinned', 'Angeheftet'),
- ('de', 'chat.list.today', 'Heute'),
- ('de', 'chat.list.yesterday', 'Gestern'),
- ('de', 'chat.list.past_7_days', 'Letzte 7 Tage'),
- ('de', 'chat.list.past_30_days', 'Letzte 30 Tage'),
- ('de', 'chat.list.older', 'Älter'),
-
- -- Chat Empty State
- ('de', 'chat.empty_state.username_default', 'dort'),
- ('de', 'chat.empty_state.greeting_morning', 'Guten Morgen'),
- ('de', 'chat.empty_state.greeting_afternoon', 'Guten Tag'),
- ('de', 'chat.empty_state.greeting_evening', 'Guten Abend'),
+ ('de', 'chat.context_menu.unpin', 'Lösen'),
('de', 'chat.empty_state.desc_1', 'Fragen Sie mich alles über Code, Mathematik oder kreatives Schreiben.'),
('de', 'chat.empty_state.desc_2', 'Ich helfe Ihnen beim Brainstorming, der Datenanalyse oder beim Schreiben von Inhalten.'),
('de', 'chat.empty_state.desc_3', 'Brauchen Sie Hilfe bei einem Projekt? Beschreiben Sie einfach, woran Sie arbeiten.'),
('de', 'chat.empty_state.desc_4', 'Ich bin hier, um bei Recherchen, Erklärungen oder Problemlösungen zu helfen.'),
('de', 'chat.empty_state.desc_5', 'Beginnen Sie ein Gespräch und lass uns Ideen gemeinsam erkunden.'),
-
- -- Code Preview
- ('de', 'chat.code_preview.title', 'Vorschau:'),
- ('de', 'chat.code_preview.preview', 'Vorschau'),
- ('de', 'chat.code_preview.code', 'Code'),
- ('de', 'chat.code_preview.copied', 'Kopiert!'),
- ('de', 'chat.code_preview.copy', 'Kopieren'),
- ('de', 'chat.code_preview.open_in_tab', 'In Tab öffnen'),
- ('de', 'chat.code_preview.close', 'Schließen'),
- ('de', 'chat.code_preview.lines', '{count} Zeilen'),
-
- -- Context Indicator
- ('de', 'chat.context_indicator.usage', 'Kontextnutzung'),
- ('de', 'chat.context_indicator.tokens', 'Tokens'),
- ('de', 'chat.context_indicator.approaching_limit', 'Kontextlimit nähert sich'),
-
- -- Message Actions
+ ('de', 'chat.empty_state.greeting_afternoon', 'Guten Tag'),
+ ('de', 'chat.empty_state.greeting_evening', 'Guten Abend'),
+ ('de', 'chat.empty_state.greeting_morning', 'Guten Morgen'),
+ ('de', 'chat.empty_state.username_default', 'dort'),
+ ('de', 'chat.image_preview.copied', 'Kopiert!'),
+ ('de', 'chat.image_preview.copy', 'URL kopieren'),
+ ('de', 'chat.image_preview.download', 'Herunterladen'),
+ ('de', 'chat.list.new_chat', 'Neuer Chat'),
+ ('de', 'chat.list.older', 'Älter'),
+ ('de', 'chat.list.past_30_days', 'Letzte 30 Tage'),
+ ('de', 'chat.list.past_7_days', 'Letzte 7 Tage'),
+ ('de', 'chat.list.pinned', 'Angeheftet'),
+ ('de', 'chat.list.today', 'Heute'),
+ ('de', 'chat.list.yesterday', 'Gestern'),
+ ('de', 'chat.message_actions.branch_to_new_chat', 'Neuen Chat abzweigen'),
('de', 'chat.message_actions.copied', 'Kopiert!'),
('de', 'chat.message_actions.copy_message', 'Nachricht kopieren'),
- ('de', 'chat.message_actions.regenerate', 'Antwort neu generieren'),
+ ('de', 'chat.message_actions.created', 'Erstellt'),
('de', 'chat.message_actions.details', 'Nachrichtendetails'),
- ('de', 'chat.message_actions.model', 'Modell'),
- ('de', 'chat.message_actions.input_tokens', 'Eingabe-Tokens'),
- ('de', 'chat.message_actions.output_tokens', 'Ausgabe-Tokens'),
- ('de', 'chat.message_actions.reasoning_tokens', 'Reasoning-Tokens'),
('de', 'chat.message_actions.input_cost', 'Eingabekosten'),
+ ('de', 'chat.message_actions.input_tokens', 'Eingabe-Tokens'),
+ ('de', 'chat.message_actions.model', 'Modell'),
('de', 'chat.message_actions.output_cost', 'Ausgabekosten'),
+ ('de', 'chat.message_actions.output_tokens', 'Ausgabe-Tokens'),
('de', 'chat.message_actions.reasoning_cost', 'Reasoning-Kosten'),
- ('de', 'chat.message_actions.total_cost', 'Gesamtkosten'),
- ('de', 'chat.message_actions.response_latency', 'Antwortlatenz'),
('de', 'chat.message_actions.reasoning_latency', 'Reasoning-Latenz'),
- ('de', 'chat.message_actions.created', 'Erstellt'),
- ('de', 'chat.message_actions.branch_to_new_chat', 'Neuen Chat abzweigen'),
-
- -- Model Selector
- ('de', 'chat.model_selector.search_models', 'Modelle suchen...'),
- ('de', 'chat.model_selector.select_model', 'Modell auswählen'),
+ ('de', 'chat.message_actions.reasoning_tokens', 'Reasoning-Tokens'),
+ ('de', 'chat.message_actions.regenerate', 'Antwort neu generieren'),
+ ('de', 'chat.message_actions.response_latency', 'Antwortlatenz'),
+ ('de', 'chat.message_actions.total_cost', 'Gesamtkosten'),
+ ('de', 'chat.message_item.branch_to_new_chat', 'Neuen Chat abzweigen'),
+ ('de', 'chat.message_item.cancel', 'Abbrechen'),
+ ('de', 'chat.message_item.copied', 'Kopiert'),
+ ('de', 'chat.message_item.copy_message', 'Nachricht kopieren'),
+ ('de', 'chat.message_item.edit_message', 'Nachricht bearbeiten'),
+ ('de', 'chat.message_item.reasoning', 'Reasoning'),
+ ('de', 'chat.message_item.save_and_fork', 'Speichern & abzweigen'),
('de', 'chat.model_selector.no_models', 'Keine Modelle gefunden'),
- ('de', 'chat.model_selector.tool_calling', 'Tool-Aufruf'),
('de', 'chat.model_selector.reasoning_capable', 'Reasoning-fähig'),
+ ('de', 'chat.model_selector.search_models', 'Modelle suchen...'),
+ ('de', 'chat.model_selector.select_model', 'Modell auswählen'),
+ ('de', 'chat.model_selector.toggle_default', 'Als Standardmodell festlegen'),
('de', 'chat.model_selector.toggle_favorite', 'Favorit umschalten'),
+ ('de', 'chat.model_selector.tool_calling', 'Tool-Aufruf'),
('de', 'chat.model_selector.vision_capable', 'Vision-fähig'),
-
-
- -- Reasoning Selector
+ ('de', 'chat.provider_selector.automatic', 'Automatisch'),
+ ('de', 'chat.provider_selector.automatic_hint', 'Das Gateway den Anbieter wählen lassen'),
+ ('de', 'chat.provider_selector.col_context', 'Kontext'),
+ ('de', 'chat.provider_selector.col_latency', 'Latenz'),
+ ('de', 'chat.provider_selector.col_price', 'Ein / Aus /M'),
+ ('de', 'chat.provider_selector.col_provider', 'Anbieter'),
+ ('de', 'chat.provider_selector.col_throughput', 'Durchsatz'),
+ ('de', 'chat.provider_selector.col_uptime', 'Verfügbarkeit'),
+ ('de', 'chat.provider_selector.context_tokens', 'Token'),
+ ('de', 'chat.provider_selector.expanded_privacy', 'Datenschutz'),
+ ('de', 'chat.provider_selector.expanded_rate_limits', 'Rate-Limits'),
+ ('de', 'chat.provider_selector.expanded_tools', 'Tools'),
+ ('de', 'chat.provider_selector.expanded_uptime', 'Verfügbarkeit'),
+ ('de', 'chat.provider_selector.latency_estimated', 'Geschätzt'),
+ ('de', 'chat.provider_selector.mode_lock', 'Sperren'),
+ ('de', 'chat.provider_selector.mode_prefer', 'Bevorzugen'),
+ ('de', 'chat.provider_selector.no_providers', 'Keine Anbieteroptionen verfügbar'),
+ ('de', 'chat.provider_selector.price_io_label', 'Ein / Aus'),
+ ('de', 'chat.provider_selector.privacy_on', 'An'),
+ ('de', 'chat.provider_selector.search_placeholder', 'Anbieter suchen...'),
+ ('de', 'chat.provider_selector.set_as_default', 'Als Standard-Anbieter festlegen'),
+ ('de', 'chat.provider_selector.sort_label', 'Sortierung'),
+ ('de', 'chat.provider_selector.sort_latency', 'Latenz'),
+ ('de', 'chat.provider_selector.sort_price', 'Preis'),
+ ('de', 'chat.provider_selector.sort_recommended', 'Empfohlen'),
+ ('de', 'chat.provider_selector.sort_throughput', 'Durchsatz'),
+ ('de', 'chat.provider_selector.sort_uptime', 'Verfügbarkeit'),
+ ('de', 'chat.provider_selector.status_all_operational', 'Alle Systeme betriebsbereit'),
+ ('de', 'chat.provider_selector.status_degraded', 'Beeinträchtigt'),
+ ('de', 'chat.provider_selector.status_healthy', 'Gesund'),
+ ('de', 'chat.provider_selector.status_some_issues', 'Einige Probleme erkannt'),
+ ('de', 'chat.provider_selector.title', 'Anbieter'),
+ ('de', 'chat.provider_selector.unavailable_for_key', 'Dieses Modell ist für den konfigurierten Schlüssel deaktiviert'),
('de', 'chat.reasoning_selector.disabled', 'Deaktiviert'),
- ('de', 'chat.reasoning_selector.token_limit', 'Token-Limit'),
- ('de', 'chat.reasoning_selector.tokens', 'Tokens'),
- ('de', 'chat.reasoning_selector.none', 'Keine'),
- ('de', 'chat.reasoning_selector.minimal', 'Minimal'),
+ ('de', 'chat.reasoning_selector.extra_high', 'Sehr Hoch'),
+ ('de', 'chat.reasoning_selector.high', 'Hoch'),
('de', 'chat.reasoning_selector.low', 'Niedrig'),
('de', 'chat.reasoning_selector.medium', 'Mittel'),
- ('de', 'chat.reasoning_selector.high', 'Hoch'),
- ('de', 'chat.reasoning_selector.extra_high', 'Sehr Hoch'),
-
- -- Tool Selector
- ('de', 'chat.tool_selector.search_tools', 'Tools suchen...'),
- ('de', 'chat.tool_selector.no_tool_support', 'Ausgewähltes Modell unterstützt keine Tools'),
- ('de', 'chat.tool_selector.no_tools_found', 'Keine Tools gefunden'),
-
- -- Streaming Message
+ ('de', 'chat.reasoning_selector.minimal', 'Minimal'),
+ ('de', 'chat.reasoning_selector.none', 'Keine'),
+ ('de', 'chat.reasoning_selector.token_limit', 'Token-Limit'),
+ ('de', 'chat.reasoning_selector.tokens', 'Tokens'),
('de', 'chat.streaming_message.assistant', 'Assistent'),
-
- -- Message Item
- ('en', 'chat.message_item.reasoning', 'Reasoning'),
- ('en', 'chat.message_item.edit_message', 'Edit message'),
- ('en', 'chat.message_item.copy_message', 'Copy message'),
- ('en', 'chat.message_item.copied', 'Copied'),
- ('en', 'chat.message_item.cancel', 'Cancel'),
- ('en', 'chat.message_item.save_and_fork', 'Save & Fork'),
- ('en', 'chat.message_item.branch_to_new_chat', 'Branch to new chat'),
- ('de', 'chat.message_item.reasoning', 'Reasoning'),
- ('de', 'chat.message_item.edit_message', 'Nachricht bearbeiten'),
- ('de', 'chat.message_item.copy_message', 'Nachricht kopieren'),
- ('de', 'chat.message_item.copied', 'Kopiert'),
- ('de', 'chat.message_item.cancel', 'Abbrechen'),
- ('de', 'chat.message_item.save_and_fork', 'Speichern & abzweigen'),
- ('de', 'chat.message_item.branch_to_new_chat', 'Neuen Chat abzweigen'),
-
- -- Tool Execution Display
- ('en', 'chat.tool_execution.tool', 'Tool'),
- ('en', 'chat.tool_execution.running', 'Running...'),
- ('en', 'chat.tool_execution.failed', 'Failed'),
- ('en', 'chat.tool_execution.complete', 'Complete'),
- ('en', 'chat.tool_execution.arguments', 'Arguments'),
- ('en', 'chat.tool_execution.output', 'Output'),
- ('en', 'chat.tool_execution.error', 'Error'),
- ('en', 'chat.tool_execution.completed_in', 'Completed in {ms}ms'),
-
- ('de', 'chat.tool_execution.tool', 'Tool'),
- ('de', 'chat.tool_execution.running', 'Wird ausgeführt...'),
- ('de', 'chat.tool_execution.failed', 'Fehlgeschlagen'),
- ('de', 'chat.tool_execution.complete', 'Abgeschlossen'),
('de', 'chat.tool_execution.arguments', 'Argumente'),
- ('de', 'chat.tool_execution.output', 'Ausgabe'),
- ('de', 'chat.tool_execution.error', 'Fehler'),
+ ('de', 'chat.tool_execution.complete', 'Abgeschlossen'),
('de', 'chat.tool_execution.completed_in', 'Abgeschlossen in {ms}ms'),
-
- -- Image Generation
- ('en', 'chat.image_preview.download', 'Download'),
- ('en', 'chat.image_preview.copy', 'Copy URL'),
- ('en', 'chat.image_preview.copied', 'Copied!'),
- ('en', 'chat.tool_execution.generated_image', 'Generated Image'),
-
- ('de', 'chat.image_preview.download', 'Herunterladen'),
- ('de', 'chat.image_preview.copy', 'URL kopieren'),
- ('de', 'chat.image_preview.copied', 'Kopiert!'),
+ ('de', 'chat.tool_execution.error', 'Fehler'),
+ ('de', 'chat.tool_execution.failed', 'Fehlgeschlagen'),
('de', 'chat.tool_execution.generated_image', 'Generiertes Bild'),
-
-
- -- Schema Builder
- ('en', 'settings.schema_builder.type', 'Type'),
- ('en', 'settings.schema_builder.default', 'Default'),
- ('en', 'settings.schema_builder.default_placeholder', 'Default value'),
- ('en', 'settings.schema_builder.description', 'Description'),
- ('en', 'settings.schema_builder.description_placeholder', 'Describe this parameter'),
- ('en', 'settings.schema_builder.options', 'Options (for dropdown)'),
- ('en', 'settings.schema_builder.options_placeholder', 'option1, option2, option3'),
- ('en', 'settings.schema_builder.options_hint', 'Comma-separated. Leave empty for free-form input.'),
- ('en', 'settings.schema_builder.add_property', 'Add Property'),
- ('en', 'settings.schema_builder.required', 'Required'),
- ('en', 'settings.schema_builder.secret', 'Secret (e.g. API key)'),
- ('en', 'settings.schema_builder.json_format', 'JSON Schema format'),
-
- ('de', 'settings.schema_builder.type', 'Typ'),
+ ('de', 'chat.tool_execution.output', 'Ausgabe'),
+ ('de', 'chat.tool_execution.running', 'Wird ausgeführt...'),
+ ('de', 'chat.tool_execution.tool', 'Tool'),
+ ('de', 'chat.tool_selector.available', 'Verfügbar'),
+ ('de', 'chat.tool_selector.clear', 'Leeren'),
+ ('de', 'chat.tool_selector.collapse', 'Einklappen'),
+ ('de', 'chat.tool_selector.count', '{count} ausgewählt'),
+ ('de', 'chat.tool_selector.enabled', 'Aktiviert'),
+ ('de', 'chat.tool_selector.expand', 'Ausklappen'),
+ ('de', 'chat.tool_selector.no_tool_support', 'Ausgewähltes Modell unterstützt keine Tools'),
+ ('de', 'chat.tool_selector.no_tools', 'Keine Werkzeuge verfügbar'),
+ ('de', 'chat.tool_selector.no_tools_found', 'Keine Tools gefunden'),
+ ('de', 'chat.tool_selector.no_tools_hint', 'Füge einen MCP-Server oder ein Werkzeug hinzu, um zu starten.'),
+ ('de', 'chat.tool_selector.save_as_defaults', 'Als Standard speichern'),
+ ('de', 'chat.tool_selector.search_tools', 'Tools suchen...'),
+ ('de', 'chat.tool_selector.select_all', 'Alle auswählen'),
+ ('de', 'chat.tool_selector.sub_tools', '{count} Unterwerkzeuge'),
+ ('de', 'chat.tool_selector.title', 'Werkzeuge'),
+ ('de', 'chat.tool_selector.tools', 'Werkzeuge'),
+ ('de', 'chat.tool_selector.unknown_server', 'Unbekannter MCP-Server'),
+ ('de', 'common.cancel', 'Abbrechen'),
+ ('de', 'common.close', 'Schließen'),
+ ('de', 'common.copied', 'Kopiert'),
+ ('de', 'common.copy_to_clipboard', 'In die Zwischenablage kopiert'),
+ ('de', 'common.create', 'Erstellen'),
+ ('de', 'common.delete', 'Löschen'),
+ ('de', 'common.error', 'Fehler'),
+ ('de', 'common.loading', 'Wird geladen...'),
+ ('de', 'common.next', 'Nächste'),
+ ('de', 'common.previous', 'Vorherige'),
+ ('de', 'common.refresh', 'Aktualisieren'),
+ ('de', 'common.remove', 'Entfernen'),
+ ('de', 'common.save', 'Speichern'),
+ ('de', 'common.user', 'Benutzer'),
+ ('de', 'error_messages.theme_import_failed', 'Fehler beim Importieren des Themes'),
+ ('de', 'guild.components.dialog_calendar.date.select', 'Datum auswählen'),
+ ('de', 'guild.components.dialog_calendar.ok', 'OK'),
+ ('de', 'guild.components.dialog_calendar.reset', 'Zurücksetzen'),
+ ('de', 'guild.components.dialog_calendar.time.label', 'Uhrzeit'),
+ ('de', 'guild.components.dialog_calendar.time.select', 'Uhrzeit auswählen'),
+ ('de', 'mcp.add', 'Server hinzufügen'),
+ ('de', 'mcp.args', 'Argumente (eines pro Zeile)'),
+ ('de', 'mcp.cancel', 'Abbrechen'),
+ ('de', 'mcp.command', 'Befehl'),
+ ('de', 'mcp.create_failed', 'Der MCP-Server konnte nicht erstellt werden.'),
+ ('de', 'mcp.delete', 'Löschen'),
+ ('de', 'mcp.delete_failed', 'Der MCP-Server konnte nicht gelöscht werden.'),
+ ('de', 'mcp.discover', 'Werkzeuge testen & finden'),
+ ('de', 'mcp.discover_failed', 'Verbindung zum MCP-Server fehlgeschlagen.'),
+ ('de', 'mcp.discovered_tools', 'Gefundene Werkzeuge'),
+ ('de', 'mcp.edit', 'Bearbeiten'),
+ ('de', 'mcp.enabled', 'Aktiviert'),
+ ('de', 'mcp.headers', 'Header (JSON)'),
+ ('de', 'mcp.headers_add', 'Header hinzufügen'),
+ ('de', 'mcp.headers_empty', 'Noch keine Header hinzugefügt.'),
+ ('de', 'mcp.headers_name', 'Header-Name'),
+ ('de', 'mcp.headers_remove', 'Header entfernen'),
+ ('de', 'mcp.headers_value', 'Header-Wert'),
+ ('de', 'mcp.health', 'Status'),
+ ('de', 'mcp.health_healthy', 'Erreichbar'),
+ ('de', 'mcp.health_unhealthy', 'Nicht erreichbar'),
+ ('de', 'mcp.health_unknown', 'Unbekannt'),
+ ('de', 'mcp.manage_description', 'Verbinde entfernte Model-Context-Protocol-Server, um deinen Chats Werkzeuge hinzuzufügen.'),
+ ('de', 'mcp.manage_title', 'MCP-Server'),
+ ('de', 'mcp.name', 'Name'),
+ ('de', 'mcp.name_placeholder', 'Mein MCP-Server'),
+ ('de', 'mcp.no_servers', 'Noch keine MCP-Server.'),
+ ('de', 'mcp.save', 'Speichern'),
+ ('de', 'mcp.tool_selector.manage', 'MCP-Server verwalten…'),
+ ('de', 'mcp.transport', 'Transport'),
+ ('de', 'mcp.transport_http', 'Streamable HTTP'),
+ ('de', 'mcp.transport_stdio', 'stdio (serverseitig)'),
+ ('de', 'mcp.update_failed', 'Der MCP-Server konnte nicht aktualisiert werden.'),
+ ('de', 'mcp.url', 'Server-URL'),
+ ('de', 'mcp.url_placeholder', 'https://example.com/mcp'),
+ ('de', 'settings.admin.default_model', 'Standardmodell'),
+ ('de', 'settings.admin.default_model_hint', 'Dieses Modell für alle Benutzer vorauswählen, wenn kein Team- oder persönliches Standardmodell festgelegt ist.'),
+ ('de', 'settings.admin_config.coming_soon', 'Konfigurationsverwaltung kommt bald'),
+ ('de', 'settings.admin_config.description', 'Anwendungskonfigurationseinstellungen'),
+ ('de', 'settings.admin_config.provider_selector.hint', 'Benutzer wählen lassen, welcher Upstream-Anbieter jede OpenRouter-Anfrage bedient.'),
+ ('de', 'settings.admin_config.provider_selector.label', 'Chat-Anbieterauswahl'),
+ ('de', 'settings.admin_users.cannot_modify_self', 'Sie können Ihr eigenes Konto nicht bearbeiten'),
+ ('de', 'settings.admin_users.coming_soon', 'Benutzerverwaltung kommt bald'),
+ ('de', 'settings.admin_users.create_error', 'Fehler beim Erstellen des Benutzers'),
+ ('de', 'settings.admin_users.create_success', 'Benutzer erstellt'),
+ ('de', 'settings.admin_users.create_user', 'Benutzer erstellen'),
+ ('de', 'settings.admin_users.create_user_description', 'Erstellen Sie einen neuen Benutzer mit optionalen Rollen und Passwort'),
+ ('de', 'settings.admin_users.delete_confirm', 'Möchten Sie {username} wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.'),
+ ('de', 'settings.admin_users.delete_error', 'Fehler beim Löschen des Benutzers'),
+ ('de', 'settings.admin_users.delete_success', 'Benutzer gelöscht'),
+ ('de', 'settings.admin_users.delete_user', 'Benutzer löschen'),
+ ('de', 'settings.admin_users.description', 'Benutzerkonten und Berechtigungen verwalten'),
+ ('de', 'settings.admin_users.edit_error', 'Fehler beim Aktualisieren des Benutzers'),
+ ('de', 'settings.admin_users.edit_success', 'Benutzer aktualisiert'),
+ ('de', 'settings.admin_users.edit_user', 'Benutzer bearbeiten'),
+ ('de', 'settings.admin_users.field_email', 'E-Mail'),
+ ('de', 'settings.admin_users.field_password', 'Passwort'),
+ ('de', 'settings.admin_users.field_roles', 'Rollen'),
+ ('de', 'settings.admin_users.field_teams', 'Teams'),
+ ('de', 'settings.admin_users.field_username', 'Benutzername'),
+ ('de', 'settings.admin_users.filter_role', 'Nach Rolle filtern'),
+ ('de', 'settings.admin_users.filter_team', 'Nach Team filtern'),
+ ('de', 'settings.admin_users.load_error', 'Benutzer konnten nicht geladen werden'),
+ ('de', 'settings.admin_users.no_users', 'Keine Benutzer gefunden'),
+ ('de', 'settings.admin_users.reset_password', 'Passwort zurücksetzen'),
+ ('de', 'settings.admin_users.reset_password_placeholder', 'Neues Passwort (leer lassen, um aktuelles zu behalten)'),
+ ('de', 'settings.admin_users.role_all', 'Alle'),
+ ('de', 'settings.admin_users.search_placeholder', 'Benutzer suchen...'),
+ ('de', 'settings.admin_users.select_roles', 'Rollen auswählen'),
+ ('de', 'settings.admin_users.select_teams', 'Teams auswählen'),
+ ('de', 'settings.analytics.api_key', 'API-Schluessel'),
+ ('de', 'settings.analytics.api_keys_soon', 'API-Schlüssel-Tracking wird in einem zukünftigen Release verfügbar sein'),
+ ('de', 'settings.analytics.avg', 'Durchschn.'),
+ ('de', 'settings.analytics.avg_cost', 'Durchschn. Kosten / Tag'),
+ ('de', 'settings.analytics.coming_soon', 'Demnächst'),
+ ('de', 'settings.analytics.cost', 'Kosten'),
+ ('de', 'settings.analytics.custom', 'Benutzerdefiniert'),
+ ('de', 'settings.analytics.custom_range', 'Benutzerdefinierter Zeitraum'),
+ ('de', 'settings.analytics.daily', 'Taeglich'),
+ ('de', 'settings.analytics.description', 'Token- und Ausgabentrends pruefen'),
+ ('de', 'settings.analytics.from', 'Von'),
+ ('de', 'settings.analytics.group_by', 'Gruppieren nach'),
+ ('de', 'settings.analytics.hourly', 'Stuendlich'),
+ ('de', 'settings.analytics.label', 'Bezeichnung'),
+ ('de', 'settings.analytics.last_30d', 'Letzte 30 Tage'),
+ ('de', 'settings.analytics.last_7d', 'Letzte 7 Tage'),
+ ('de', 'settings.analytics.last_90d', 'Letzte 90 Tage'),
+ ('de', 'settings.analytics.latency', 'Latenz'),
+ ('de', 'settings.analytics.max', 'Max'),
+ ('de', 'settings.analytics.metric', 'Metrik'),
+ ('de', 'settings.analytics.min', 'Min'),
+ ('de', 'settings.analytics.model', 'Modell'),
+ ('de', 'settings.analytics.my_usage', 'Meine Nutzung'),
+ ('de', 'settings.analytics.no_data', 'Keine Daten fuer diesen Zeitraum verfuegbar'),
+ ('de', 'settings.analytics.none', 'Keine'),
+ ('de', 'settings.analytics.other', 'Andere'),
+ ('de', 'settings.analytics.per_day', 'pro Tag'),
+ ('de', 'settings.analytics.preset_last_30d', 'Letzte 30 Tage'),
+ ('de', 'settings.analytics.preset_last_7d', 'Letzte 7 Tage'),
+ ('de', 'settings.analytics.preset_last_90d', 'Letzte 90 Tage'),
+ ('de', 'settings.analytics.preset_last_month', 'Letzten Monat'),
+ ('de', 'settings.analytics.preset_this_month', 'Diesen Monat'),
+ ('de', 'settings.analytics.preset_this_year', 'Dieses Jahr'),
+ ('de', 'settings.analytics.provider', 'Provider'),
+ ('de', 'settings.analytics.reasoning_tokens', 'Reasoning-Tokens'),
+ ('de', 'settings.analytics.request_count', 'Anfragen'),
+ ('de', 'settings.analytics.request_volume', 'Anfrageanzahl nach Modell'),
+ ('de', 'settings.analytics.requests', 'Anfragen'),
+ ('de', 'settings.analytics.rollup', 'Rollup'),
+ ('de', 'settings.analytics.section_api_keys', 'API-Schlüssel'),
+ ('de', 'settings.analytics.section_models', 'Modelle'),
+ ('de', 'settings.analytics.section_user', 'Benutzer'),
+ ('de', 'settings.analytics.spend', 'Ausgaben'),
+ ('de', 'settings.analytics.spend_over_time', 'Ausgaben ueber Zeit'),
+ ('de', 'settings.analytics.sum', 'Summe'),
+ ('de', 'settings.analytics.tab_explore', 'Erkunden'),
+ ('de', 'settings.analytics.tab_overview', 'Uebersicht'),
+ ('de', 'settings.analytics.tab_trends', 'Trends'),
+ ('de', 'settings.analytics.to', 'Bis'),
+ ('de', 'settings.analytics.token_split', 'Token-Aufteilung'),
+ ('de', 'settings.analytics.tokens', 'Tokens'),
+ ('de', 'settings.analytics.tokens_completion', 'Tokens (Completion)'),
+ ('de', 'settings.analytics.tokens_input', 'Tokens (Eingabe)'),
+ ('de', 'settings.analytics.tokens_output', 'Tokens (Ausgabe)'),
+ ('de', 'settings.analytics.tokens_prompt', 'Tokens (Prompt)'),
+ ('de', 'settings.analytics.tokens_reasoning', 'Tokens (Reasoning)'),
+ ('de', 'settings.analytics.tokens_total', 'Tokens (gesamt)'),
+ ('de', 'settings.analytics.top', 'Top'),
+ ('de', 'settings.analytics.top_api_keys', 'Top API-Schlüssel'),
+ ('de', 'settings.analytics.top_models', 'Nach Modell'),
+ ('de', 'settings.analytics.top_teams', 'Nach Team'),
+ ('de', 'settings.analytics.top_users', 'Nach Benutzer'),
+ ('de', 'settings.analytics.total', 'Gesamt'),
+ ('de', 'settings.analytics.total_cost', 'Gesamtkosten'),
+ ('de', 'settings.analytics.total_tokens', 'Gesamte Tokens'),
+ ('de', 'settings.analytics.total_tokens_short', 'Tokens'),
+ ('de', 'settings.analytics.total_usage_dollars', 'Gesamtnutzung ($)'),
+ ('de', 'settings.analytics.trend', 'Trend'),
+ ('de', 'settings.analytics.trending', 'Trend'),
+ ('de', 'settings.analytics.usage_by_model', 'Nutzung nach Modell'),
+ ('de', 'settings.analytics.user', 'Benutzer'),
+ ('de', 'settings.analytics.value', 'Wert'),
+ ('de', 'settings.analytics.weekly', 'Woechentlich'),
+ ('de', 'settings.appearance.built_in_themes', 'Eingebaute Themes'),
+ ('de', 'settings.appearance.custom_themes', 'Meine Themes'),
+ ('de', 'settings.appearance.description', 'Passen Sie das Aussehen an'),
+ ('de', 'settings.appearance.display_mode', 'Anzeigemodus'),
+ ('de', 'settings.appearance.display_mode_description', 'Wählen Sie zwischen Hell- und Dunkelmodus'),
+ ('de', 'settings.appearance.import_theme', 'Theme importieren'),
+ ('de', 'settings.appearance.language', 'Sprache'),
+ ('de', 'settings.appearance.language_de', 'Deutsch'),
+ ('de', 'settings.appearance.language_description', 'Wählen Sie Ihre bevorzugte Sprache'),
+ ('de', 'settings.appearance.language_en', 'Englisch'),
+ ('de', 'settings.appearance.loading_themes', 'Themes werden geladen...'),
+ ('de', 'settings.appearance.no_themes_found', 'Keine Themes gefunden'),
+ ('de', 'settings.appearance.random_theme', 'Zufälliges Theme'),
+ ('de', 'settings.appearance.reset_theme', 'Theme zurücksetzen'),
+ ('de', 'settings.appearance.search_themes', 'Themes suchen...'),
+ ('de', 'settings.appearance.theme', 'Theme'),
+ ('de', 'settings.appearance.theme_dark', 'Dunkel'),
+ ('de', 'settings.appearance.theme_description', 'Wählen Sie zwischen Hell- und Dunkelmodus'),
+ ('de', 'settings.appearance.theme_light', 'Hell'),
+ ('de', 'settings.appearance.theme_system', 'System'),
+ ('de', 'settings.appearance.themes', 'Themes'),
+ ('de', 'settings.appearance.themes_description', 'Farbthemen auswählen und verwalten'),
+ ('de', 'settings.appearance.themes_powered_by', 'Mehr Themes bei'),
+ ('de', 'settings.appearance.title', 'Erscheinungsbild'),
+ ('de', 'settings.appearance.try_different_search', 'Versuchen Sie eine andere Suchanfrage'),
+ ('de', 'settings.back', 'Zurück'),
+ ('de', 'settings.budgets.affected_users', 'betroffen'),
+ ('de', 'settings.budgets.all_kinds', 'Alle Arten'),
+ ('de', 'settings.budgets.amount', 'Betrag'),
+ ('de', 'settings.budgets.assign_error', 'Budget konnte nicht zugewiesen werden'),
+ ('de', 'settings.budgets.assign_success', 'Budget zugewiesen'),
+ ('de', 'settings.budgets.assign_team', 'Team zuweisen'),
+ ('de', 'settings.budgets.assign_user', 'Benutzer zuweisen'),
+ ('de', 'settings.budgets.assignment', 'Zuweisung'),
+ ('de', 'settings.budgets.assignments', 'Zuweisungen'),
+ ('de', 'settings.budgets.budget', 'Budget'),
+ ('de', 'settings.budgets.budget_count', 'Budgets'),
+ ('de', 'settings.budgets.create', 'Budget erstellen'),
+ ('de', 'settings.budgets.create_error', 'Budget konnte nicht erstellt werden'),
+ ('de', 'settings.budgets.create_success', 'Budget erstellt'),
+ ('de', 'settings.budgets.delete_error', 'Budget konnte nicht geloescht werden'),
+ ('de', 'settings.budgets.delete_success', 'Budget geloescht'),
+ ('de', 'settings.budgets.description', 'Ausgaben fuer Benutzer und Teams begrenzen'),
+ ('de', 'settings.budgets.effective_budgets', 'Wirksame Budgets'),
+ ('de', 'settings.budgets.enabled', 'Aktiv'),
+ ('de', 'settings.budgets.interval', 'Intervall'),
+ ('de', 'settings.budgets.kind', 'Art'),
+ ('de', 'settings.budgets.kind_scope', 'Art'),
+ ('de', 'settings.budgets.members', 'Mitglieder'),
+ ('de', 'settings.budgets.name', 'Name'),
+ ('de', 'settings.budgets.no_assignments', 'Noch keine Zuweisungen'),
+ ('de', 'settings.budgets.no_reset', 'Kein Reset'),
+ ('de', 'settings.budgets.no_reset_history', 'Noch keine Reset-Historie'),
+ ('de', 'settings.budgets.no_team_budgets', 'Keine Teambudgets gefunden'),
+ ('de', 'settings.budgets.no_user_budgets', 'Keine Benutzerbudgets gefunden'),
+ ('de', 'settings.budgets.notes', 'Beschreibung'),
+ ('de', 'settings.budgets.on_exceed', 'Aktion'),
+ ('de', 'settings.budgets.pick_team', 'Team auswaehlen…'),
+ ('de', 'settings.budgets.pick_user', 'Benutzer auswaehlen…'),
+ ('de', 'settings.budgets.reached', 'Budget erreicht'),
+ ('de', 'settings.budgets.reason', 'Grund'),
+ ('de', 'settings.budgets.reason_placeholder', 'Optionale Notiz fuer das Audit-Log'),
+ ('de', 'settings.budgets.remaining', 'Verbleibend'),
+ ('de', 'settings.budgets.remove_error', 'Zuweisung konnte nicht entfernt werden'),
+ ('de', 'settings.budgets.remove_success', 'Zuweisung entfernt'),
+ ('de', 'settings.budgets.reset', 'Zuruecksetzen'),
+ ('de', 'settings.budgets.reset_at', 'Reset um'),
+ ('de', 'settings.budgets.reset_budget', 'Budget zuruecksetzen'),
+ ('de', 'settings.budgets.reset_by', 'Zurueckgesetzt von'),
+ ('de', 'settings.budgets.reset_error', 'Budget konnte nicht zurueckgesetzt werden'),
+ ('de', 'settings.budgets.reset_keeps_history', 'Die Nutzungshistorie bleibt erhalten. Die Budgetberechnung startet fuer diesen Bereich ab jetzt neu.'),
+ ('de', 'settings.budgets.reset_strategy', 'Zuruecksetzen'),
+ ('de', 'settings.budgets.reset_success', 'Budget zurueckgesetzt'),
+ ('de', 'settings.budgets.save_error', 'Budget konnte nicht gespeichert werden'),
+ ('de', 'settings.budgets.save_success', 'Budget gespeichert'),
+ ('de', 'settings.budgets.scope', 'Bereich'),
+ ('de', 'settings.budgets.search_teams', 'Teams suchen'),
+ ('de', 'settings.budgets.search_users', 'Benutzer suchen'),
+ ('de', 'settings.budgets.settings', 'Einstellungen'),
+ ('de', 'settings.budgets.status', 'Status'),
+ ('de', 'settings.budgets.tab_budgets', 'Budgets'),
+ ('de', 'settings.budgets.tab_history', 'Historie'),
+ ('de', 'settings.budgets.tab_teams', 'Teams'),
+ ('de', 'settings.budgets.tab_users', 'Benutzer'),
+ ('de', 'settings.budgets.team', 'Team'),
+ ('de', 'settings.budgets.team_budget', 'Teambudget'),
+ ('de', 'settings.budgets.teams', 'Teams'),
+ ('de', 'settings.budgets.used', 'Verbraucht'),
+ ('de', 'settings.budgets.user', 'Benutzer'),
+ ('de', 'settings.budgets.user_budget', 'Benutzerbudget'),
+ ('de', 'settings.budgets.users_with_budgets', 'Benutzer'),
+ ('de', 'settings.description', 'Verwalten Sie Ihre Kontoeinstellungen und Konfiguration.'),
+ ('de', 'settings.import', 'Importieren'),
+ ('de', 'settings.import_theme', 'Theme importieren'),
+ ('de', 'settings.import_theme_description', 'Fügen Sie eine Theme-URL von'),
+ ('de', 'settings.models.active', 'Aktiviert'),
+ ('de', 'settings.models.description', 'Verwalte und konfiguriere verfügbare KI-Modelle.'),
+ ('de', 'settings.models.disabled', 'Deaktiviert'),
+ ('de', 'settings.models.editor.change_icon', 'Modell-Icon'),
+ ('de', 'settings.models.editor.characters', 'Zeichen'),
+ ('de', 'settings.models.editor.core_prompt', 'System-Prompt'),
+ ('de', 'settings.models.editor.core_prompt_desc', 'System-Prompt, der bei jeder Unterhaltung mit diesem Modell verwendet wird.'),
+ ('de', 'settings.models.editor.description', 'Beschreibung'),
+ ('de', 'settings.models.editor.description_empty_preview', 'Nichts zum Anzeigen'),
+ ('de', 'settings.models.editor.description_markdown_hint', 'Markdown wird unterstützt'),
+ ('de', 'settings.models.editor.description_placeholder', 'Beschreibe dieses Modell mit Markdown...'),
+ ('de', 'settings.models.editor.description_tab_preview', 'Vorschau'),
+ ('de', 'settings.models.editor.description_tab_write', 'Schreiben'),
+ ('de', 'settings.models.editor.discard_changes', 'Verwerfen & Zurück'),
+ ('de', 'settings.models.editor.display_name', 'Anzeigename'),
+ ('de', 'settings.models.editor.display_name_placeholder', 'z.B. GPT-4o'),
+ ('de', 'settings.models.editor.effective_price', 'Effektiver Preis'),
+ ('de', 'settings.models.editor.enable_model', 'Modell aktivieren'),
+ ('de', 'settings.models.editor.enable_model_desc', 'Dieses Modell für Benutzer verfügbar machen.'),
+ ('de', 'settings.models.editor.frequency_penalty_tooltip', 'Bestraft häufig vorgekommene Tokens. Positive Werte reduzieren Wiederholungen; negative Werte fördern sie.'),
+ ('de', 'settings.models.editor.general', 'Allgemeine Konfiguration'),
+ ('de', 'settings.models.editor.general_info', 'Allgemeine Informationen'),
+ ('de', 'settings.models.editor.general_info_desc', 'Grundlegende Identitäts- und Verfügbarkeitseinstellungen.'),
+ ('de', 'settings.models.editor.icon_choose_file', 'Bild auswählen'),
+ ('de', 'settings.models.editor.icon_clear', 'Icon entfernen'),
+ ('de', 'settings.models.editor.icon_tab_upload', 'Hochladen'),
+ ('de', 'settings.models.editor.icon_tab_url', 'URL'),
+ ('de', 'settings.models.editor.icon_upload_hint', 'Lade ein lokales Bild als Modell-Icon hoch.'),
+ ('de', 'settings.models.editor.icon_url', 'Icon-URL'),
+ ('de', 'settings.models.editor.icon_url_placeholder', 'https://example.com/icon.png'),
+ ('de', 'settings.models.editor.is_enabled', 'Aktiviert'),
+ ('de', 'settings.models.editor.keep_editing', 'Weiter bearbeiten'),
+ ('de', 'settings.models.editor.loading', 'Modell wird geladen...'),
+ ('de', 'settings.models.editor.logprobs', 'Log-Wahrscheinlichkeiten'),
+ ('de', 'settings.models.editor.logprobs_enable', 'Log-Wahrscheinlichkeiten zurückgeben'),
+ ('de', 'settings.models.editor.logprobs_tooltip', 'Gibt die Log-Wahrscheinlichkeiten der Ausgabe-Tokens zurück.'),
+ ('de', 'settings.models.editor.max_tokens', 'Max. Tokens'),
+ ('de', 'settings.models.editor.max_tokens_tooltip', 'Maximale Anzahl der zu generierenden Tokens in der Antwort.'),
+ ('de', 'settings.models.editor.model_id', 'Modell-ID'),
+ ('de', 'settings.models.editor.override_cache_read', 'Cache-Lese-Ueberschreibung'),
+ ('de', 'settings.models.editor.override_cache_write', 'Cache-Schreib-Ueberschreibung'),
+ ('de', 'settings.models.editor.override_input', 'Eingabe-Ueberschreibung'),
+ ('de', 'settings.models.editor.override_output', 'Ausgabe-Ueberschreibung'),
+ ('de', 'settings.models.editor.override_reasoning', 'Reasoning-Ueberschreibung'),
+ ('de', 'settings.models.editor.parallel_tool_calls', 'Parallele Tool-Aufrufe'),
+ ('de', 'settings.models.editor.parallel_tool_calls_enable', 'Parallele Tool-Aufrufe aktivieren'),
+ ('de', 'settings.models.editor.parallel_tool_calls_tooltip', 'Erlaubt dem Modell, mehrere Tools gleichzeitig in einem Zug aufzurufen.'),
+ ('de', 'settings.models.editor.param_group_advanced', 'Erweitert'),
+ ('de', 'settings.models.editor.param_group_core', 'Kern'),
+ ('de', 'settings.models.editor.param_group_penalties', 'Strafen'),
+ ('de', 'settings.models.editor.param_group_reasoning', 'Reasoning'),
+ ('de', 'settings.models.editor.param_group_token', 'Token-Sampling'),
+ ('de', 'settings.models.editor.parameters', 'Parameter'),
+ ('de', 'settings.models.editor.parameters_desc', 'Standard-Sampling-Parameter. Nur aktivierte Parameter werden an das Modell gesendet; deaktivierte nutzen die Anbieter-Standardwerte.'),
+ ('de', 'settings.models.editor.presence_penalty_tooltip', 'Bestraft Tokens, die bereits erschienen sind. Ermutigt das Modell, neue Themen zu besprechen.'),
+ ('de', 'settings.models.editor.pricing', 'Preise'),
+ ('de', 'settings.models.editor.pricing_deleted', 'Preis-Ueberschreibung entfernt'),
+ ('de', 'settings.models.editor.pricing_desc', 'Abrechnungspreise pro eine Million Tokens ueberschreiben.'),
+ ('de', 'settings.models.editor.pricing_saved', 'Preise gespeichert'),
+ ('de', 'settings.models.editor.pricing_unit', 'Preise gelten pro eine Million Tokens. Optionale Felder leer lassen, um sie nicht zu setzen.'),
+ ('de', 'settings.models.editor.provider_col_context', 'Kontext'),
+ ('de', 'settings.models.editor.provider_col_latency', 'Latenz'),
+ ('de', 'settings.models.editor.provider_col_price', 'Preis (Ein/Aus pro 1M)'),
+ ('de', 'settings.models.editor.provider_col_provider', 'Anbieter'),
+ ('de', 'settings.models.editor.provider_col_quant', 'Quantisierung'),
+ ('de', 'settings.models.editor.provider_col_status', 'Status'),
+ ('de', 'settings.models.editor.provider_col_throughput', 'Durchsatz'),
+ ('de', 'settings.models.editor.provider_col_uptime', 'Verfügbarkeit'),
+ ('de', 'settings.models.editor.providers', 'Anbieter'),
+ ('de', 'settings.models.editor.providers_desc', 'Anbieter-Endpunkte, die dieses Modell bereitstellen können, mit Status und Preisen.'),
+ ('de', 'settings.models.editor.providers_empty', 'Keine Anbieteroptionen verfügbar'),
+ ('de', 'settings.models.editor.providers_load_error', 'Anbieteroptionen konnten nicht geladen werden'),
+ ('de', 'settings.models.editor.providers_refresh', 'Aktualisieren'),
+ ('de', 'settings.models.editor.providers_user_unavailable', 'Dieses Modell ist für den konfigurierten API-Schlüssel deaktiviert.'),
+ ('de', 'settings.models.editor.reasoning_budget', 'Reasoning-Token-Budget'),
+ ('de', 'settings.models.editor.reasoning_budget_tooltip', 'Maximale Anzahl von Tokens, die das Modell für internes Reasoning verwenden darf.'),
+ ('de', 'settings.models.editor.reasoning_effort', 'Reasoning-Aufwand'),
+ ('de', 'settings.models.editor.reasoning_effort_placeholder', 'Aufwandsstufe auswählen'),
+ ('de', 'settings.models.editor.reasoning_effort_tooltip', 'Steuert, wie viel Reasoning das Modell vor der Antwortgenerierung durchführt.'),
+ ('de', 'settings.models.editor.reported_price', 'Gemeldeter Preis'),
+ ('de', 'settings.models.editor.row_available', 'Verfügbar'),
+ ('de', 'settings.models.editor.row_unavailable', 'Nicht verfügbar'),
+ ('de', 'settings.models.editor.row_unknown', 'Unbekannt'),
+ ('de', 'settings.models.editor.row_user_unavailable', 'Für diesen Schlüssel deaktiviert'),
+ ('de', 'settings.models.editor.sampling', 'Sampling-Parameter'),
+ ('de', 'settings.models.editor.sampling_desc', 'Standard-Sampling-Parameter für dieses Modell.'),
+ ('de', 'settings.models.editor.seed', 'Seed'),
+ ('de', 'settings.models.editor.seed_placeholder', 'z.B. 42'),
+ ('de', 'settings.models.editor.seed_tooltip', 'Zufalls-Seed für deterministische Generierung. Gleicher Seed mit gleichem Prompt sollte dieselbe Ausgabe erzeugen.'),
+ ('de', 'settings.models.editor.stop_sequences', 'Stop-Sequenzen'),
+ ('de', 'settings.models.editor.stop_sequences_placeholder', 'Stop-String eingeben und Enter drücken'),
+ ('de', 'settings.models.editor.stop_sequences_tooltip', 'Das Modell stoppt die Generierung, wenn es eine dieser Sequenzen erzeugt.'),
+ ('de', 'settings.models.editor.system_prompt', 'System-Prompt'),
+ ('de', 'settings.models.editor.system_prompt_desc', 'Basis-System-Prompt, der auf alle Konversationen mit diesem Modell angewendet wird.'),
+ ('de', 'settings.models.editor.system_prompt_hint', 'Variablen wie {{user_name}} werden zur Laufzeit ersetzt.'),
+ ('de', 'settings.models.editor.system_prompt_placeholder', 'Du bist ein hilfreicher KI-Assistent...'),
+ ('de', 'settings.models.editor.temperature', 'Temperatur'),
+ ('de', 'settings.models.editor.temperature_label_high', 'kreativ'),
+ ('de', 'settings.models.editor.temperature_label_low', 'präzise'),
+ ('de', 'settings.models.editor.temperature_precise', 'präzise ← → kreativ'),
+ ('de', 'settings.models.editor.temperature_tooltip', 'Steuert die Zufälligkeit. Niedrigere Werte erzeugen deterministischere Ausgaben; höhere Werte erhöhen die Kreativität.'),
+ ('de', 'settings.models.editor.top_k_tooltip', 'Begrenzt die Tokenauswahl auf die K wahrscheinlichsten Tokens. 0 deaktiviert diesen Filter.'),
+ ('de', 'settings.models.editor.top_logprobs', 'Top Log-Wahrscheinlichkeiten'),
+ ('de', 'settings.models.editor.top_logprobs_tooltip', 'Anzahl der wahrscheinlichsten Tokens je Position, zusammen mit ihren Log-Wahrscheinlichkeiten.'),
+ ('de', 'settings.models.editor.top_p', 'Top P'),
+ ('de', 'settings.models.editor.top_p_tooltip', 'Nucleus-Sampling-Schwellenwert. Es werden nur Tokens berücksichtigt, deren kumulierte Wahrscheinlichkeitsmasse diesen Wert erreicht.'),
+ ('de', 'settings.models.editor.unsaved_changes', 'Nicht gespeicherte Änderungen'),
+ ('de', 'settings.models.editor.unsaved_dialog_desc', 'Du hast nicht gespeicherte Änderungen, die verloren gehen. Möchtest du wirklich zurückgehen?'),
+ ('de', 'settings.models.editor.unsaved_dialog_title', 'Nicht gespeicherte Änderungen'),
+ ('de', 'settings.models.editor.upload', 'Hochladen'),
+ ('de', 'settings.models.editor.var_date', 'Aktuelles UTC-Datum (JJJJ-MM-TT)'),
+ ('de', 'settings.models.editor.var_datetime', 'Aktuelles UTC-Datum und Uhrzeit (JJJJ-MM-TT HH:MM)'),
+ ('de', 'settings.models.editor.var_model_id', 'Bezeichner-String des Modells'),
+ ('de', 'settings.models.editor.var_model_name', 'Anzeigename des Modells'),
+ ('de', 'settings.models.editor.var_time', 'Aktuelle UTC-Zeit (HH:MM)'),
+ ('de', 'settings.models.editor.var_user_email', 'E-Mail-Adresse des eingeloggten Nutzers'),
+ ('de', 'settings.models.editor.var_user_name', 'Benutzername des eingeloggten Nutzers'),
+ ('de', 'settings.models.editor.variables_panel_desc', 'Diese Platzhalter werden beim Senden des System-Prompts an das Modell ersetzt.'),
+ ('de', 'settings.models.editor.variables_panel_title', 'Verfügbare Variablen'),
+ ('de', 'settings.models.no_models', 'Keine Modelle gefunden.'),
+ ('de', 'settings.models.not_found', 'Modell nicht gefunden.'),
+ ('de', 'settings.models.save_error', 'Fehler beim Speichern der Modellkonfiguration.'),
+ ('de', 'settings.models.save_success', 'Modellkonfiguration erfolgreich gespeichert.'),
+ ('de', 'settings.models.search', 'Modelle suchen...'),
+ ('de', 'settings.profile.chat_defaults', 'Chat-Standardeinstellungen'),
+ ('de', 'settings.profile.chat_defaults_description', 'Standardmäßiges Modell, Anbieter und Tools für jeden neuen Chat vorkonfigurieren.'),
+ ('de', 'settings.profile.default_model', 'Standardmodell'),
+ ('de', 'settings.profile.default_model_hint', 'Dieses Modell beim Starten eines neuen Chats vorauswählen. Überschreibt Team- und globale Standards.'),
+ ('de', 'settings.profile.default_provider', 'Standard-Anbieter'),
+ ('de', 'settings.profile.default_provider_hint', 'Einen vorgelagerten Anbieter für das Standardmodell vorauswählen. Nur für Gateway-Modelle sichtbar.'),
+ ('de', 'settings.profile.default_provider_placeholder', 'Automatisch'),
+ ('de', 'settings.profile.default_tools', 'Standard-Tools'),
+ ('de', 'settings.profile.default_tools_hint', 'Diese Tools werden beim Starten eines neuen Chats vorausgewählt.'),
+ ('de', 'settings.profile.description', 'Ihre persönlichen Kontoinformationen'),
+ ('de', 'settings.profile.edit', 'Bearbeiten'),
+ ('de', 'settings.profile.email', 'E-Mail'),
+ ('de', 'settings.profile.name', 'Name'),
+ ('de', 'settings.profile.no_tools_available', 'Keine Tools verfügbar. Verbinde einen MCP-Server oder füge Tools in den Tool-Einstellungen hinzu.'),
+ ('de', 'settings.profile.save_error', 'Chat-Standardeinstellungen konnten nicht gespeichert werden.'),
+ ('de', 'settings.profile.save_success', 'Chat-Standardeinstellungen gespeichert.'),
+ ('de', 'settings.profile.title', 'Profilinformationen'),
+ ('de', 'settings.profile.use_higher_default', 'Team- / globalen Standard verwenden'),
+ ('de', 'settings.profile.user_id', 'Benutzer-ID'),
+ ('de', 'settings.providers.add_custom', 'Benutzerdefinierten Anbieter hinzufügen'),
+ ('de', 'settings.providers.anthropic_description', 'Greift auf Opus 4.5, Sonnet 4.5 und andere Anthropic-Modelle zu'),
+ ('de', 'settings.providers.api_key', 'API-Schlüssel'),
+ ('de', 'settings.providers.api_key_hint', 'Ihr API-Schlüssel wird sicher gespeichert. Wenn zudem die Verschlüsselung konfiguriert ist, wird er zudem verschlüsselt'),
+ ('de', 'settings.providers.api_key_placeholder', 'Geben Sie Ihren API-Schlüssel ein'),
+ ('de', 'settings.providers.base_url', 'Basis-URL'),
+ ('de', 'settings.providers.billing.admin_key', 'OpenAI-Admin-API-Schluessel'),
+ ('de', 'settings.providers.billing.admin_key_hint', 'Ein separater Organisations-Admin-Schluessel, der nur zum Lesen der Projektkosten verwendet wird.'),
+ ('de', 'settings.providers.billing.credits_remaining', '{amount} Guthaben verbleibend'),
+ ('de', 'settings.providers.billing.enable', 'Upstream-Abrechnung aktivieren'),
+ ('de', 'settings.providers.billing.encryption_required', 'ENCRYPTION_KEY muss gesetzt sein, bevor Abrechnungszugangsdaten gespeichert werden.'),
+ ('de', 'settings.providers.billing.failed', 'Letzte Aktualisierung fehlgeschlagen'),
+ ('de', 'settings.providers.billing.key_limit', 'Schluessellimit: {amount}'),
+ ('de', 'settings.providers.billing.local_month', '{amount} diesen Monat'),
+ ('de', 'settings.providers.billing.local_only', 'OxideChat erfasst lokale Ausgaben. Fuer diesen Anbieter gibt es keine unterstuetzte Upstream-Abrechnungsintegration.'),
+ ('de', 'settings.providers.billing.local_tracked', 'Von OxideChat erfasst'),
+ ('de', 'settings.providers.billing.management_key', 'OpenRouter-Management-Schluessel (optional)'),
+ ('de', 'settings.providers.billing.management_key_hint', 'Ein Management-Schluessel zeigt kontoweite Guthaben und Ausgaben. Ohne diesen werden Daten des Inferenzschluessels verwendet.'),
+ ('de', 'settings.providers.billing.never_updated', 'Noch nicht aktualisiert'),
+ ('de', 'settings.providers.billing.over_threshold', 'Schwellenwert um {amount} ueberschritten'),
+ ('de', 'settings.providers.billing.period', 'Zeitraum'),
+ ('de', 'settings.providers.billing.project_id', 'OpenAI-Projekt-ID'),
+ ('de', 'settings.providers.billing.project_name', 'Projekt-Anzeigename (optional)'),
+ ('de', 'settings.providers.billing.provider_reported', 'Vom Anbieter gemeldet'),
+ ('de', 'settings.providers.billing.refresh', 'Aktualisieren'),
+ ('de', 'settings.providers.billing.refresh_failed', 'Abrechnungsaktualisierung fehlgeschlagen'),
+ ('de', 'settings.providers.billing.remaining_threshold', '{amount} bis zum Schwellenwert von {threshold}'),
+ ('de', 'settings.providers.billing.remove', 'Abrechnungsverbindung entfernen'),
+ ('de', 'settings.providers.billing.removed', 'Abrechnungsverbindung entfernt'),
+ ('de', 'settings.providers.billing.saved', 'Abrechnungseinstellungen gespeichert'),
+ ('de', 'settings.providers.billing.setup_required', 'Abrechnung muss eingerichtet werden'),
+ ('de', 'settings.providers.billing.spent_month', '{amount} diesen Monat ausgegeben'),
+ ('de', 'settings.providers.billing.stale', 'Veraltet'),
+ ('de', 'settings.providers.billing.tab', 'Abrechnung'),
+ ('de', 'settings.providers.billing.threshold', 'Ausgabenschwellenwert'),
+ ('de', 'settings.providers.billing.title', 'Abrechnungsuebersicht'),
+ ('de', 'settings.providers.billing.unsupported', 'Vom Anbieter nicht unterstuetzt'),
+ ('de', 'settings.providers.billing.updated', 'Aktualisiert {time}'),
+ ('de', 'settings.providers.catalog_available', 'Verfügbar'),
+ ('de', 'settings.providers.catalog_disabled_key', 'Für diesen Schlüssel deaktiviert'),
+ ('de', 'settings.providers.catalog_empty', 'Keine Katalogmodelle gefunden'),
+ ('de', 'settings.providers.catalog_search', 'Katalogmodelle suchen...'),
+ ('de', 'settings.providers.configure', 'Konfigurieren'),
+ ('de', 'settings.providers.configure_description', 'Geben Sie Ihre API-Anmeldedaten ein, um diesen Anbieter zu aktivieren'),
+ ('de', 'settings.providers.configured', 'Konfiguriert'),
+ ('de', 'settings.providers.delete_error', 'Fehler beim Entfernen des Anbieters'),
+ ('de', 'settings.providers.delete_success', 'Anbieter erfolgreich entfernt'),
+ ('de', 'settings.providers.description', 'Verwenden Sie Ihre eigenen API-Schlüssel für erweiterte AI-Funktionen'),
+ ('de', 'settings.providers.edit', 'Konfigurieren'),
+ ('de', 'settings.providers.enabled', 'Anbieter aktivieren'),
+ ('de', 'settings.providers.enabled_hint', 'Diesen Anbieter für AI-Anfragen verwenden'),
+ ('de', 'settings.providers.google_description', 'Greift auf Gemini 3 Familie und andere Google-Modelle zu'),
+ ('de', 'settings.providers.name', 'Anzeigename'),
+ ('de', 'settings.providers.openai_description', 'Greift auf GPT-5.2, GPT-5.2-Codex und andere OpenAI-Modelle zu'),
+ ('de', 'settings.providers.openrouter_description', 'Greift auf eine Vielzahl von Modellen über OpenRouter zu'),
+ ('de', 'settings.providers.save_error', 'Fehler beim Speichern der Anbieterkonfiguration'),
+ ('de', 'settings.providers.save_success', 'Anbieterkonfiguration erfolgreich gespeichert'),
+ ('de', 'settings.providers.syncing_provider', 'Synchronisiere Modelle'),
+ ('de', 'settings.providers.syncing_provider_description', 'Modelle des Anbieters werden synchronisiert, dies kann einen Moment dauern'),
+ ('de', 'settings.providers.syncing_provider_success', 'Modelle erfolgreich synchronisiert'),
+ ('de', 'settings.providers.tab_catalog', 'Katalog'),
+ ('de', 'settings.providers.tab_settings', 'Einstellungen'),
+ ('de', 'settings.providers.title', 'BYOK AI-Anbieter'),
+ ('de', 'settings.providers.toggling_provider', 'Anbieter umschalten'),
+ ('de', 'settings.providers.toggling_provider_description', 'Bitte warten Sie, während der Anbieter umgeschaltet wird'),
+ ('de', 'settings.providers.toggling_provider_success', 'Anbieter erfolgreich umgeschaltet'),
+ ('de', 'settings.schema_builder.add_property', 'Eigenschaft hinzufügen'),
('de', 'settings.schema_builder.default', 'Standard'),
('de', 'settings.schema_builder.default_placeholder', 'Standardwert'),
('de', 'settings.schema_builder.description', 'Beschreibung'),
('de', 'settings.schema_builder.description_placeholder', 'Beschreiben Sie diesen Parameter'),
+ ('de', 'settings.schema_builder.json_format', 'JSON-Schema-Format'),
('de', 'settings.schema_builder.options', 'Optionen (für Dropdown)'),
- ('de', 'settings.schema_builder.options_placeholder', 'Option1, Option2, Option3'),
('de', 'settings.schema_builder.options_hint', 'Kommagetrennt. Leer lassen für freie Eingabe.'),
- ('de', 'settings.schema_builder.add_property', 'Eigenschaft hinzufügen'),
+ ('de', 'settings.schema_builder.options_placeholder', 'Option1, Option2, Option3'),
('de', 'settings.schema_builder.required', 'Erforderlich'),
('de', 'settings.schema_builder.secret', 'Geheim (z.B. API-Schlüssel)'),
- ('de', 'settings.schema_builder.json_format', 'JSON-Schema-Format'),
-
- -- Tool Test Dialog
- ('en', 'settings.tool_test.test', 'Test'),
- ('en', 'settings.tool_test.description', 'Enter input values to test this tool'),
- ('en', 'settings.tool_test.select_function', 'Select Function'),
- ('en', 'settings.tool_test.select_placeholder', 'Select a function...'),
- ('en', 'settings.tool_test.form', 'Form'),
- ('en', 'settings.tool_test.no_parameters', 'This tool has no input parameters'),
- ('en', 'settings.tool_test.array_placeholder', 'Comma-separated values'),
- ('en', 'settings.tool_test.array_hint', 'Enter values separated by commas'),
- ('en', 'settings.tool_test.success', 'Success'),
- ('en', 'settings.tool_test.failed', 'Failed'),
- ('en', 'settings.tool_test.view_output', 'View output'),
- ('en', 'settings.tool_test.run_test', 'Run Test'),
-
- ('de', 'settings.tool_test.test', 'Testen'),
+ ('de', 'settings.schema_builder.type', 'Typ'),
+ ('de', 'settings.sessions.current', 'Aktuell'),
+ ('de', 'settings.sessions.description', 'Verwalten Sie Ihre aktiven Anmeldesitzungen'),
+ ('de', 'settings.sessions.last_seen', 'Zuletzt gesehen'),
+ ('de', 'settings.sessions.sign_out', 'Abmelden'),
+ ('de', 'settings.sessions.title', 'Aktive Sitzungen'),
+ ('de', 'settings.tabs.admin_config', 'Konfiguration'),
+ ('de', 'settings.tabs.admin_users', 'Benutzer'),
+ ('de', 'settings.tabs.ai_options', 'AI Optionen'),
+ ('de', 'settings.tabs.analytics', 'Analysen'),
+ ('de', 'settings.tabs.appearance', 'Erscheinungsbild'),
+ ('de', 'settings.tabs.budgets', 'Budgets'),
+ ('de', 'settings.tabs.models', 'Modelle'),
+ ('de', 'settings.tabs.profile', 'Profil'),
+ ('de', 'settings.tabs.providers', 'Anbieter'),
+ ('de', 'settings.tabs.teams', 'Teams'),
+ ('de', 'settings.tabs.tools', 'Tools'),
+ ('de', 'settings.tabs.usage_analytics', 'Benutzungsstatistiken'),
+ ('de', 'settings.teams.allow_all_models', 'Alle Modelle erlauben'),
+ ('de', 'settings.teams.allow_all_models_hint', 'Mitglieder können jedes aktivierte Modell nutzen. Deaktivieren, um den Zugriff einzuschränken.'),
+ ('de', 'settings.teams.budget', 'Budget'),
+ ('de', 'settings.teams.budget_link_hint', 'Verknüpfe dieses Team mit einer Budget-Konfiguration, um Nutzungslimits zu verfolgen und durchzusetzen.'),
+ ('de', 'settings.teams.budget_link_title', 'Budget-Konfiguration'),
+ ('de', 'settings.teams.budget_linked', 'Verknüpftes Budget'),
+ ('de', 'settings.teams.budget_none_available', 'Es existieren noch keine Budget-Konfigurationen. Sobald eine erstellt wurde, kannst du sie hier verknüpfen.'),
+ ('de', 'settings.teams.budget_not_linked', 'Kein Budget verknüpft'),
+ ('de', 'settings.teams.budget_unlink', 'Trennen'),
+ ('de', 'settings.teams.confirm_delete', 'Dieses Team löschen? Mitglieder behalten den Zugriff über ihre anderen Teams.'),
+ ('de', 'settings.teams.create', 'Team erstellen'),
+ ('de', 'settings.teams.create_error', 'Team konnte nicht erstellt werden'),
+ ('de', 'settings.teams.create_success', 'Team erstellt'),
+ ('de', 'settings.teams.default', 'Standard'),
+ ('de', 'settings.teams.default_model', 'Standardmodell'),
+ ('de', 'settings.teams.default_model_hint', 'Dieses Modell für Teammitglieder vorauswählen. Überschreibt das globale Standardmodell.'),
+ ('de', 'settings.teams.delete_error', 'Team konnte nicht gelöscht werden'),
+ ('de', 'settings.teams.delete_success', 'Team gelöscht'),
+ ('de', 'settings.teams.description', 'Benutzer, Modellzugriff und Budgets organisieren'),
+ ('de', 'settings.teams.description_field', 'Beschreibung'),
+ ('de', 'settings.teams.load_error', 'Teams konnten nicht geladen werden'),
+ ('de', 'settings.teams.members', 'Mitglieder'),
+ ('de', 'settings.teams.members_count', '{count} Mitglieder'),
+ ('de', 'settings.teams.models', 'Modelle'),
+ ('de', 'settings.teams.name', 'Name'),
+ ('de', 'settings.teams.no_members', 'Keine Benutzer gefunden'),
+ ('de', 'settings.teams.no_models', 'Keine Modelle gefunden'),
+ ('de', 'settings.teams.no_selection', 'Wähle ein Team, um Mitglieder, Modelle und Budget zu verwalten.'),
+ ('de', 'settings.teams.no_selection_title', 'Kein Team ausgewählt'),
+ ('de', 'settings.teams.providers', 'Anbieter'),
+ ('de', 'settings.teams.save_error', 'Team konnte nicht gespeichert werden'),
+ ('de', 'settings.teams.save_success', 'Team gespeichert'),
+ ('de', 'settings.teams.search', 'Teams suchen...'),
+ ('de', 'settings.teams.search_members', 'Mitglieder suchen...'),
+ ('de', 'settings.teams.search_models', 'Modelle suchen...'),
+ ('de', 'settings.teams.selected_count', '{count} ausgewählt'),
+ ('de', 'settings.teams.use_global_default', 'Globalen Standard verwenden'),
+ ('de', 'settings.theme_deleted', 'Theme gelöscht'),
+ ('de', 'settings.theme_imported', 'Theme erfolgreich importiert'),
+ ('de', 'settings.theme_saved', 'Theme-Einstellungen gespeichert'),
+ ('de', 'settings.theme_url', 'Theme-URL'),
+ ('de', 'settings.title', 'Einstellungen'),
+ ('de', 'settings.tool_test.array_hint', 'Geben Sie Werte durch Kommas getrennt ein'),
+ ('de', 'settings.tool_test.array_placeholder', 'Kommagetrennte Werte'),
('de', 'settings.tool_test.description', 'Geben Sie Eingabewerte ein, um dieses Tool zu testen'),
- ('de', 'settings.tool_test.select_function', 'Funktion auswählen'),
- ('de', 'settings.tool_test.select_placeholder', 'Wählen Sie eine Funktion...'),
+ ('de', 'settings.tool_test.failed', 'Fehlgeschlagen'),
('de', 'settings.tool_test.form', 'Formular'),
('de', 'settings.tool_test.no_parameters', 'Dieses Tool hat keine Eingabeparameter'),
- ('de', 'settings.tool_test.array_placeholder', 'Kommagetrennte Werte'),
- ('de', 'settings.tool_test.array_hint', 'Geben Sie Werte durch Kommas getrennt ein'),
- ('de', 'settings.tool_test.success', 'Erfolgreich'),
- ('de', 'settings.tool_test.failed', 'Fehlgeschlagen'),
- ('de', 'settings.tool_test.view_output', 'Ausgabe anzeigen'),
('de', 'settings.tool_test.run_test', 'Test ausführen'),
-
- -- Tools Settings
- ('en', 'settings.tabs.tools', 'Tools'),
- ('en', 'settings.tools.title', 'Tools'),
- ('en', 'settings.tools.description', 'Manage custom tools that AI models can use during conversations'),
- ('en', 'settings.tools.add', 'Add Tool'),
- ('en', 'settings.tools.no_tools', 'No tools yet'),
- ('en', 'settings.tools.no_tools_description', 'Create custom tools to extend AI capabilities'),
- ('en', 'settings.tools.create_first', 'Create your first tool'),
- ('en', 'settings.tools.template', 'Template'),
- ('en', 'settings.tools.configured', 'Configured'),
- ('en', 'settings.tools.no_description', 'No description'),
- ('en', 'settings.tools.configure', 'Configure'),
- ('en', 'settings.tools.edit', 'Edit Tool'),
- ('en', 'settings.tools.create', 'Create Tool'),
- ('en', 'settings.tools.general', 'General'),
- ('en', 'settings.tools.source', 'Source'),
- ('en', 'settings.tools.functions', 'Functions'),
- ('en', 'settings.tools.identifier', 'Identifier'),
- ('en', 'settings.tools.identifier_placeholder', 'fetch_website'),
- ('en', 'settings.tools.display_name', 'Display Name'),
- ('en', 'settings.tools.display_name_placeholder', 'Fetch Website'),
- ('en', 'settings.tools.description', 'Description'),
- ('en', 'settings.tools.description_placeholder', 'Fetches content from a URL'),
- ('en', 'settings.tools.soon', 'Soon'),
- ('en', 'settings.tools.url_placeholder', 'https://api.example.com/{{input.query}}'),
- ('en', 'settings.tools.headers', 'Headers (JSON)'),
- ('en', 'settings.tools.headers_placeholder', '{"Authorization": "Bearer {{settings.api_key}}"}'),
- ('en', 'settings.tools.stdio', 'Stdio (Local Process)'),
- ('en', 'settings.tools.sse', 'SSE (HTTP)'),
- ('en', 'settings.tools.command_placeholder', 'Command (e.g. npx)'),
- ('en', 'settings.tools.args_placeholder', 'Arguments (comma-separated)'),
- ('en', 'settings.tools.tool_name_placeholder', 'Tool name from server'),
- ('en', 'settings.tools.settings_schema', 'Settings Schema (optional)'),
- ('en', 'settings.tools.new', 'New'),
- ('en', 'settings.tools.name', 'Name'),
- ('en', 'settings.tools.function_name_placeholder', 'search_web'),
- ('en', 'settings.tools.entrypoint', 'Entrypoint'),
- ('en', 'settings.tools.optional', 'Optional'),
- ('en', 'settings.tools.function_description_placeholder', 'What this function does'),
- ('en', 'settings.tools.input_schema', 'Input Schema'),
- ('en', 'settings.tools.remove', 'Remove'),
- ('en', 'settings.tools.settings', 'Tool Settings'),
- ('en', 'settings.tools.settings_description', 'Configure your personal settings for {name}'),
- ('en', 'settings.tools.select_placeholder', 'Select...'),
-
- ('de', 'settings.tabs.tools', 'Tools'),
- ('de', 'settings.tools.title', 'Tools'),
- ('de', 'settings.tools.description', 'Verwalten Sie benutzerdefinierte Tools, die KI-Modelle während Gesprächen verwenden können'),
+ ('de', 'settings.tool_test.select_function', 'Funktion auswählen'),
+ ('de', 'settings.tool_test.select_placeholder', 'Wählen Sie eine Funktion...'),
+ ('de', 'settings.tool_test.success', 'Erfolgreich'),
+ ('de', 'settings.tool_test.test', 'Testen'),
+ ('de', 'settings.tool_test.view_output', 'Ausgabe anzeigen'),
('de', 'settings.tools.add', 'Tool hinzufügen'),
- ('de', 'settings.tools.no_tools', 'Noch keine Tools'),
- ('de', 'settings.tools.no_tools_description', 'Erstellen Sie benutzerdefinierte Tools, um die KI-Fähigkeiten zu erweitern'),
- ('de', 'settings.tools.create_first', 'Erstellen Sie Ihren ersten Tool'),
- ('de', 'settings.tools.template', 'Vorlage'),
- ('de', 'settings.tools.configured', 'Konfiguriert'),
- ('de', 'settings.tools.no_description', 'Keine Beschreibung'),
+ ('de', 'settings.tools.args_placeholder', 'Argumente (kommagetrennt)'),
+ ('de', 'settings.tools.command_placeholder', 'Befehl (z.B. npx)'),
('de', 'settings.tools.configure', 'Konfigurieren'),
- ('de', 'settings.tools.edit', 'Tool bearbeiten'),
+ ('de', 'settings.tools.configured', 'Konfiguriert'),
('de', 'settings.tools.create', 'Tool erstellen'),
- ('de', 'settings.tools.general', 'Allgemein'),
- ('de', 'settings.tools.source', 'Quelle'),
- ('de', 'settings.tools.functions', 'Funktionen'),
- ('de', 'settings.tools.identifier', 'Bezeichner'),
- ('de', 'settings.tools.identifier_placeholder', 'website_aufrufen'),
- ('de', 'settings.tools.display_name', 'Anzeigename'),
- ('de', 'settings.tools.display_name_placeholder', 'Website abrufen'),
+ ('de', 'settings.tools.create_first', 'Erstellen Sie Ihren ersten Tool'),
('de', 'settings.tools.description', 'Beschreibung'),
('de', 'settings.tools.description_placeholder', 'Ruft Inhalte von einer URL ab'),
- ('de', 'settings.tools.soon', 'Bald'),
- ('de', 'settings.tools.url_placeholder', 'https://api.example.com/{{input.query}}'),
+ ('de', 'settings.tools.display_name', 'Anzeigename'),
+ ('de', 'settings.tools.display_name_placeholder', 'Website abrufen'),
+ ('de', 'settings.tools.edit', 'Tool bearbeiten'),
+ ('de', 'settings.tools.entrypoint', 'Einstiegspunkt'),
+ ('de', 'settings.tools.function_description_placeholder', 'Was diese Funktion tut'),
+ ('de', 'settings.tools.function_name_placeholder', 'web_suchen'),
+ ('de', 'settings.tools.functions', 'Funktionen'),
+ ('de', 'settings.tools.general', 'Allgemein'),
('de', 'settings.tools.headers', 'Header (JSON)'),
('de', 'settings.tools.headers_placeholder', '{"Authorization": "Bearer {{settings.api_key}}"}'),
- ('de', 'settings.tools.stdio', 'Stdio (Lokaler Prozess)'),
- ('de', 'settings.tools.sse', 'SSE (HTTP)'),
- ('de', 'settings.tools.command_placeholder', 'Befehl (z.B. npx)'),
- ('de', 'settings.tools.args_placeholder', 'Argumente (kommagetrennt)'),
- ('de', 'settings.tools.tool_name_placeholder', 'Tool-Name vom Server'),
- ('de', 'settings.tools.settings_schema', 'Einstellungsschema (optional)'),
- ('de', 'settings.tools.new', 'Neu'),
+ ('de', 'settings.tools.identifier', 'Bezeichner'),
+ ('de', 'settings.tools.identifier_placeholder', 'website_aufrufen'),
+ ('de', 'settings.tools.image_model_description', 'Wählen Sie das globale Bildmodell aus, das dieses Tool verwendet.'),
+ ('de', 'settings.tools.image_model_placeholder', 'Bildmodell auswählen'),
+ ('de', 'settings.tools.image_model_title', 'Bildmodell'),
+ ('de', 'settings.tools.imagegen.description', 'Bilder mit einem von der Administration ausgewählten Anbietermodell erstellen und bearbeiten'),
+ ('de', 'settings.tools.imagegen.display_name', 'Bildgenerierung'),
+ ('de', 'settings.tools.imagegen.edit.description', 'Ein zuvor erstelltes Bild anhand einer Texteingabe bearbeiten'),
+ ('de', 'settings.tools.imagegen.edit.image_id', 'Die Bild-ID eines zuvor erstellten Bildes. Diese wird gegenüber einer Bild-URL bevorzugt.'),
+ ('de', 'settings.tools.imagegen.edit.image_url', 'URL eines externen Bildes. Nur verwenden, wenn keine Bild-ID verfügbar ist.'),
+ ('de', 'settings.tools.imagegen.edit.prompt', 'Die Texteingabe, die die gewünschte Bearbeitung beschreibt'),
+ ('de', 'settings.tools.imagegen.generate.description', 'Ein Bild anhand einer Texteingabe erstellen'),
+ ('de', 'settings.tools.imagegen.generate.prompt', 'Die Texteingabe, die das zu erstellende Bild beschreibt'),
+ ('de', 'settings.tools.imagegen.generate.quality', 'Bildqualität'),
+ ('de', 'settings.tools.imagegen.generate.size', 'Bildgröße'),
+ ('de', 'settings.tools.imagegen.settings.description', 'Ein aktiviertes Bildmodell auswählen'),
+ ('de', 'settings.tools.input_schema', 'Eingabeschema'),
('de', 'settings.tools.name', 'Name'),
- ('de', 'settings.tools.function_name_placeholder', 'web_suchen'),
- ('de', 'settings.tools.entrypoint', 'Einstiegspunkt'),
+ ('de', 'settings.tools.new', 'Neu'),
+ ('de', 'settings.tools.no_description', 'Keine Beschreibung'),
+ ('de', 'settings.tools.no_tools', 'Noch keine Tools'),
+ ('de', 'settings.tools.no_tools_description', 'Erstellen Sie benutzerdefinierte Tools, um die KI-Fähigkeiten zu erweitern'),
('de', 'settings.tools.optional', 'Optional'),
- ('de', 'settings.tools.function_description_placeholder', 'Was diese Funktion tut'),
- ('de', 'settings.tools.input_schema', 'Eingabeschema'),
('de', 'settings.tools.remove', 'Entfernen'),
+ ('de', 'settings.tools.select_placeholder', 'Auswählen...'),
('de', 'settings.tools.settings', 'Tool-Einstellungen'),
('de', 'settings.tools.settings_description', 'Konfigurieren Sie Ihre persönlichen Einstellungen für {name}'),
- ('de', 'settings.tools.select_placeholder', 'Auswählen...'),
-
- -- Display Mode section (English)
- ('en', 'settings.appearance.display_mode', 'Display Mode'),
- ('en', 'settings.appearance.display_mode_description', 'Choose between light and dark mode'),
-
- -- Themes section (English)
- ('en', 'settings.appearance.themes', 'Themes'),
- ('en', 'settings.appearance.themes_description', 'Select and manage your color themes'),
- ('en', 'settings.appearance.search_themes', 'Search themes...'),
- ('en', 'settings.appearance.random_theme', 'Random theme'),
- ('en', 'settings.appearance.reset_theme', 'Reset theme'),
- ('en', 'settings.appearance.import_theme', 'Import Theme'),
- ('en', 'settings.appearance.loading_themes', 'Loading themes...'),
- ('en', 'settings.appearance.custom_themes', 'My Themes'),
- ('en', 'settings.appearance.built_in_themes', 'Built-in Themes'),
- ('en', 'settings.appearance.no_themes_found', 'No themes found'),
- ('en', 'settings.appearance.try_different_search', 'Try adjusting your search query'),
- ('en', 'settings.appearance.themes_powered_by', 'Get more themes at'),
- ('en', 'settings.theme_deleted', 'Theme deleted'),
- ('en', 'settings.theme_saved', 'Theme preferences saved'),
-
- -- Display Mode section (German)
- ('de', 'settings.appearance.display_mode', 'Anzeigemodus'),
- ('de', 'settings.appearance.display_mode_description', 'Wählen Sie zwischen Hell- und Dunkelmodus'),
-
- -- Themes section (German)
- ('de', 'settings.appearance.themes', 'Themes'),
- ('de', 'settings.appearance.themes_description', 'Farbthemen auswählen und verwalten'),
- ('de', 'settings.appearance.search_themes', 'Themes suchen...'),
- ('de', 'settings.appearance.random_theme', 'Zufälliges Theme'),
- ('de', 'settings.appearance.reset_theme', 'Theme zurücksetzen'),
- ('de', 'settings.appearance.import_theme', 'Theme importieren'),
- ('de', 'settings.appearance.loading_themes', 'Themes werden geladen...'),
- ('de', 'settings.appearance.custom_themes', 'Meine Themes'),
- ('de', 'settings.appearance.built_in_themes', 'Eingebaute Themes'),
- ('de', 'settings.appearance.no_themes_found', 'Keine Themes gefunden'),
- ('de', 'settings.appearance.try_different_search', 'Versuchen Sie eine andere Suchanfrage'),
- ('de', 'settings.appearance.themes_powered_by', 'Mehr Themes bei'),
- ('de', 'settings.theme_deleted', 'Theme gelöscht'),
- ('de', 'settings.theme_saved', 'Theme-Einstellungen gespeichert'),
-
- -- Admin Users
- ('en', 'settings.admin_users.create_user', 'Create User'),
- ('de', 'settings.admin_users.create_user', 'Benutzer erstellen'),
- ('en', 'settings.admin_users.search_placeholder', 'Search users...'),
- ('de', 'settings.admin_users.search_placeholder', 'Benutzer suchen...'),
- ('en', 'settings.admin_users.filter_role', 'Filter role'),
- ('de', 'settings.admin_users.filter_role', 'Nach Rolle filtern'),
- ('en', 'settings.admin_users.role_all', 'All'),
- ('de', 'settings.admin_users.role_all', 'Alle'),
- ('en', 'settings.admin_users.no_users', 'No users found'),
- ('de', 'settings.admin_users.no_users', 'Keine Benutzer gefunden'),
+ ('de', 'settings.tools.settings_schema', 'Einstellungsschema (optional)'),
+ ('de', 'settings.tools.soon', 'Bald'),
+ ('de', 'settings.tools.source', 'Quelle'),
+ ('de', 'settings.tools.sse', 'SSE (HTTP)'),
+ ('de', 'settings.tools.stdio', 'Stdio (Lokaler Prozess)'),
+ ('de', 'settings.tools.system_prompt', 'System-Prompt-Einschub'),
+ ('de', 'settings.tools.system_prompt_hint', 'Wird dem System-Prompt vorangestellt, sobald dieses Tool für eine Anfrage aktiviert ist. Damit steuern Sie, wie das Modell das Tool verwendet.'),
+ ('de', 'settings.tools.system_prompt_placeholder', 'Zusätzliche Systemanweisungen, wenn dieses Tool aktiv ist'),
+ ('de', 'settings.tools.template', 'Vorlage'),
+ ('de', 'settings.tools.title', 'Tools'),
+ ('de', 'settings.tools.tool_name_placeholder', 'Tool-Name vom Server'),
+ ('de', 'settings.tools.url_placeholder', 'https://api.example.com/{{input.query}}'),
+ ('de', 'sidebar.ai_chat_app', 'KI-Chat-Anwendung'),
+ ('de', 'sidebar.all', 'Alle'),
+ ('de', 'sidebar.all_chats', 'Alle Chats'),
+ ('de', 'sidebar.budget.remaining', '{amount} uebrig'),
+ ('de', 'sidebar.chats', 'Chats'),
+ ('de', 'sidebar.logout', 'Abmelden'),
+ ('de', 'sidebar.manage_mcp', 'MCP-Server verwalten'),
+ ('de', 'sidebar.manage_workspaces', 'Arbeitsbereiche verwalten'),
+ ('de', 'sidebar.new_chat', 'Neuer Chat'),
+ ('de', 'sidebar.workspace', 'Arbeitsbereich'),
+ ('de', 'welcome.description', 'Wählen Sie einen Chat aus der Seitenleiste oder beginnen Sie eine neue Konversation'),
+ ('de', 'welcome.title', 'Willkommen bei OxideChat'),
+ ('de', 'workspace.cancel', 'Abbrechen'),
+ ('de', 'workspace.cannot_delete_default', 'Der Standard-Arbeitsbereich kann nicht gelöscht werden.'),
+ ('de', 'workspace.chat_count', '{count} Chats'),
+ ('de', 'workspace.color', 'Akzentfarbe'),
+ ('de', 'workspace.color_none', 'Keine Farbe'),
+ ('de', 'workspace.confirm', 'Bestätigen'),
+ ('de', 'workspace.create', 'Arbeitsbereich erstellen'),
+ ('de', 'workspace.create_failed', 'Arbeitsbereich konnte nicht erstellt werden.'),
+ ('de', 'workspace.default_badge', 'Standard'),
+ ('de', 'workspace.delete', 'Löschen'),
+ ('de', 'workspace.delete_action_archive', 'Chats archivieren'),
+ ('de', 'workspace.delete_action_delete', 'Chats endgültig löschen'),
+ ('de', 'workspace.delete_action_move', 'Chats in einen anderen Arbeitsbereich verschieben'),
+ ('de', 'workspace.delete_description', 'Wähle, was mit den Chats in "{name}" passiert.'),
+ ('de', 'workspace.delete_failed', 'Arbeitsbereich konnte nicht gelöscht werden.'),
+ ('de', 'workspace.delete_move_target', 'Verschieben nach'),
+ ('de', 'workspace.delete_title', 'Arbeitsbereich löschen'),
+ ('de', 'workspace.empty', 'Noch keine Arbeitsbereiche.'),
+ ('de', 'workspace.manage_description', 'Arbeitsbereiche erstellen, umbenennen, einfärben und löschen.'),
+ ('de', 'workspace.manage_title', 'Arbeitsbereiche'),
+ ('de', 'workspace.name', 'Name'),
+ ('de', 'workspace.name_placeholder', 'Name des Arbeitsbereichs'),
+ ('de', 'workspace.rename', 'Umbenennen'),
+ ('de', 'workspace.save', 'Speichern'),
+ ('de', 'workspace.set_default', 'Als Standard festlegen'),
+ ('de', 'workspace.update_failed', 'Arbeitsbereich konnte nicht aktualisiert werden.'),
+ ('en', 'admin.tools.mcp.allow_stdio', 'Allow server-side stdio MCP servers'),
+ ('en', 'admin.tools.mcp.allow_stdio_hint', 'When enabled, global stdio MCP servers run arbitrary commands on the OxideChat host. Only enable on trusted, single-tenant instances.'),
+ ('en', 'auth.errors.back_to_login', 'Back to Login'),
+ ('en', 'auth.errors.email_or_username_taken', 'Email or username already taken'),
+ ('en', 'auth.errors.email_taken', 'This email is already registered'),
+ ('en', 'auth.errors.external_auth', 'This account uses external authentication'),
+ ('en', 'auth.errors.go_home', 'Go Home'),
+ ('en', 'auth.errors.internal_error', 'An error occurred. Please try again.'),
+ ('en', 'auth.errors.invalid_credentials', 'Invalid email or password'),
+ ('en', 'auth.errors.invalid_email', 'Please enter a valid email address'),
+ ('en', 'auth.errors.not_authenticated', 'Not authenticated'),
+ ('en', 'auth.errors.oauth_email_conflict', 'An account with this email already exists. Please log in with your original method.'),
+ ('en', 'auth.errors.oauth_email_conflict_help', 'Your OAuth provider returned an email that is already associated with an existing account. To link your OAuth identity, please log in with your existing credentials first, then link the OAuth provider from your account settings.'),
+ ('en', 'auth.errors.oauth_email_conflict_title', 'Account Already Exists'),
+ ('en', 'auth.errors.oauth_state_mismatch', 'OAuth authentication failed due to a state mismatch. Please try again.'),
+ ('en', 'auth.errors.oauth_state_mismatch_title', 'Authentication Failed'),
+ ('en', 'auth.errors.oauth_token_error', 'Failed to obtain authentication token. Please try again.'),
+ ('en', 'auth.errors.oauth_token_error_title', 'Token Error'),
+ ('en', 'auth.errors.oauth_user_info_error', 'Failed to retrieve user information from the OAuth provider.'),
+ ('en', 'auth.errors.oauth_user_info_error_title', 'User Info Error'),
+ ('en', 'auth.errors.password_no_digit', 'Password must contain at least one number'),
+ ('en', 'auth.errors.password_no_lowercase', 'Password must contain at least one lowercase letter'),
+ ('en', 'auth.errors.password_no_special', 'Password must contain at least one special character'),
+ ('en', 'auth.errors.password_no_uppercase', 'Password must contain at least one uppercase letter'),
+ ('en', 'auth.errors.password_requirements_not_met', 'Please ensure your password meets all requirements'),
+ ('en', 'auth.errors.password_too_long', 'Password must be at most 128 characters'),
+ ('en', 'auth.errors.password_too_short', 'Password must be at least 8 characters'),
+ ('en', 'auth.errors.setup_completed', 'Setup has already been completed'),
+ ('en', 'auth.errors.setup_required', 'Setup must be completed first'),
+ ('en', 'auth.errors.unknown_error', 'An unexpected error occurred. Please try again.'),
+ ('en', 'auth.errors.unknown_error_title', 'Error'),
+ ('en', 'auth.errors.username_invalid_chars', 'Username can only contain letters, numbers, underscores, and hyphens'),
+ ('en', 'auth.errors.username_taken', 'This username is already taken'),
+ ('en', 'auth.errors.username_too_long', 'Username must be at most 32 characters'),
+ ('en', 'auth.errors.username_too_short', 'Username must be at least 3 characters'),
+ ('en', 'auth.login.and', 'and'),
+ ('en', 'auth.login.description', 'Login with your favorite external provider'),
+ ('en', 'auth.login.discord', 'Login with Discord'),
+ ('en', 'auth.login.email', 'Email'),
+ ('en', 'auth.login.forgot_password', 'Forgot your password?'),
+ ('en', 'auth.login.google', 'Login with Google'),
+ ('en', 'auth.login.no_account', 'Don''t have an account?'),
+ ('en', 'auth.login.or_continue', 'Or continue with'),
+ ('en', 'auth.login.password', 'Password'),
+ ('en', 'auth.login.privacy_link', 'Privacy Policy'),
+ ('en', 'auth.login.sign_up', 'Sign up'),
+ ('en', 'auth.login.submit', 'Login'),
+ ('en', 'auth.login.submitting', 'Signing in...'),
+ ('en', 'auth.login.terms_link', 'Terms of Service'),
+ ('en', 'auth.login.terms_prefix', 'By clicking continue, you agree to our'),
+ ('en', 'auth.login.title', 'Welcome back'),
+ ('en', 'auth.messages.logout_success', 'Logged out successfully'),
+ ('en', 'auth.password.digit', 'One number'),
+ ('en', 'auth.password.lowercase', 'One lowercase letter'),
+ ('en', 'auth.password.max_length', 'Maximum {max} characters'),
+ ('en', 'auth.password.min_length', 'At least {min} characters'),
+ ('en', 'auth.password.special', 'One special character'),
+ ('en', 'auth.password.strength_fair', 'Fair'),
+ ('en', 'auth.password.strength_good', 'Good'),
+ ('en', 'auth.password.strength_weak', 'Weak'),
+ ('en', 'auth.password.uppercase', 'One uppercase letter'),
+ ('en', 'auth.register.confirm_password', 'Confirm Password'),
+ ('en', 'auth.register.description', 'Sign up with your favorite external provider'),
+ ('en', 'auth.register.discord', 'Sign up with Discord'),
+ ('en', 'auth.register.google', 'Sign up with Google'),
+ ('en', 'auth.register.have_account', 'Already have an account?'),
+ ('en', 'auth.register.password_requirements', 'At least 8 characters'),
+ ('en', 'auth.register.passwords_mismatch', 'Passwords do not match'),
+ ('en', 'auth.register.sign_in', 'Sign in'),
+ ('en', 'auth.register.submit', 'Create Account'),
+ ('en', 'auth.register.submitting', 'Creating account...'),
+ ('en', 'auth.register.title', 'Create an account'),
+ ('en', 'auth.register.username', 'Username'),
+ ('en', 'auth.setup.description', 'No users exist yet. Create your admin account to get started.'),
+ ('en', 'auth.setup.one_time_tip', 'This is a one-time setup. The first account will have administrator privileges.'),
+ ('en', 'auth.setup.submit', 'Create Admin Account'),
+ ('en', 'auth.setup.title', 'Welcome to OxideChat'),
+ ('en', 'chat.code_preview.close', 'Close'),
+ ('en', 'chat.code_preview.code', 'Code'),
+ ('en', 'chat.code_preview.copied', 'Copied'),
+ ('en', 'chat.code_preview.copy', 'Copy'),
+ ('en', 'chat.code_preview.lines', '{count} lines'),
+ ('en', 'chat.code_preview.open_in_tab', 'Open in Tab'),
+ ('en', 'chat.code_preview.preview', 'Preview'),
+ ('en', 'chat.code_preview.title', 'Preview:'),
+ ('en', 'chat.composer.hint', 'Press Enter to send, Shift+Enter for new line'),
+ ('en', 'chat.composer.placeholder_default', 'Select a model and start typing...'),
+ ('en', 'chat.composer.placeholder_model', 'Message {model}...'),
+ ('en', 'chat.context_indicator.approaching_limit', 'Approaching context limit'),
+ ('en', 'chat.context_indicator.tokens', 'tokens'),
+ ('en', 'chat.context_indicator.usage', 'Context Usage'),
+ ('en', 'chat.context_menu.archive', 'Archive'),
+ ('en', 'chat.context_menu.delete', 'Delete'),
+ ('en', 'chat.context_menu.delete_confirm', 'Are you sure you want to delete this chat? This cannot be undone.'),
+ ('en', 'chat.context_menu.export', 'Export'),
+ ('en', 'chat.context_menu.move_to', 'Move to...'),
+ ('en', 'chat.context_menu.no_workspace', 'No workspace'),
+ ('en', 'chat.context_menu.pin', 'Pin'),
+ ('en', 'chat.context_menu.rename', 'Rename'),
+ ('en', 'chat.context_menu.rename_prompt', 'Enter new title:'),
+ ('en', 'chat.context_menu.unarchive', 'Unarchive'),
+ ('en', 'chat.context_menu.unpin', 'Unpin'),
+ ('en', 'chat.empty_state.desc_1', 'Ask me anything about code, math, or creative writing.'),
+ ('en', 'chat.empty_state.desc_2', 'I can help you brainstorm, analyze data, or write content.'),
+ ('en', 'chat.empty_state.desc_3', 'Need help with a project? Just describe what you''re working on.'),
+ ('en', 'chat.empty_state.desc_4', 'I''m here to assist with research, explanations, or problem-solving.'),
+ ('en', 'chat.empty_state.desc_5', 'Start a conversation and let''s explore ideas together.'),
+ ('en', 'chat.empty_state.greeting_afternoon', 'Good afternoon'),
+ ('en', 'chat.empty_state.greeting_evening', 'Good evening'),
+ ('en', 'chat.empty_state.greeting_morning', 'Good morning'),
+ ('en', 'chat.empty_state.username_default', 'there'),
+ ('en', 'chat.image_preview.copied', 'Copied!'),
+ ('en', 'chat.image_preview.copy', 'Copy URL'),
+ ('en', 'chat.image_preview.download', 'Download'),
+ ('en', 'chat.list.new_chat', 'New chat'),
+ ('en', 'chat.list.older', 'Older'),
+ ('en', 'chat.list.past_30_days', 'Past 30 days'),
+ ('en', 'chat.list.past_7_days', 'Past 7 days'),
+ ('en', 'chat.list.pinned', 'Pinned'),
+ ('en', 'chat.list.today', 'Today'),
+ ('en', 'chat.list.yesterday', 'Yesterday'),
+ ('en', 'chat.message_actions.branch_to_new_chat', 'Branch to new chat'),
+ ('en', 'chat.message_actions.copied', 'Copied!'),
+ ('en', 'chat.message_actions.copy_message', 'Copy message'),
+ ('en', 'chat.message_actions.created', 'Created'),
+ ('en', 'chat.message_actions.details', 'Message Details'),
+ ('en', 'chat.message_actions.input_cost', 'Input cost'),
+ ('en', 'chat.message_actions.input_tokens', 'Input tokens'),
+ ('en', 'chat.message_actions.model', 'Model'),
+ ('en', 'chat.message_actions.output_cost', 'Output cost'),
+ ('en', 'chat.message_actions.output_tokens', 'Output tokens'),
+ ('en', 'chat.message_actions.reasoning_cost', 'Reasoning cost'),
+ ('en', 'chat.message_actions.reasoning_latency', 'Reasoning latency'),
+ ('en', 'chat.message_actions.reasoning_tokens', 'Reasoning tokens'),
+ ('en', 'chat.message_actions.regenerate', 'Regenerate response'),
+ ('en', 'chat.message_actions.response_latency', 'Response latency'),
+ ('en', 'chat.message_actions.total_cost', 'Total cost'),
+ ('en', 'chat.message_item.branch_to_new_chat', 'Branch to new chat'),
+ ('en', 'chat.message_item.cancel', 'Cancel'),
+ ('en', 'chat.message_item.copied', 'Copied'),
+ ('en', 'chat.message_item.copy_message', 'Copy message'),
+ ('en', 'chat.message_item.edit_message', 'Edit message'),
+ ('en', 'chat.message_item.reasoning', 'Reasoning'),
+ ('en', 'chat.message_item.save_and_fork', 'Save & Fork'),
+ ('en', 'chat.model_selector.no_models', 'No models found'),
+ ('en', 'chat.model_selector.reasoning_capable', 'Reasoning capable'),
+ ('en', 'chat.model_selector.search_models', 'Search models...'),
+ ('en', 'chat.model_selector.select_model', 'Select model'),
+ ('en', 'chat.model_selector.toggle_default', 'Set as default model'),
+ ('en', 'chat.model_selector.toggle_favorite', 'Toggle favorite'),
+ ('en', 'chat.model_selector.tool_calling', 'Tool calling'),
+ ('en', 'chat.model_selector.vision_capable', 'Vision capable'),
+ ('en', 'chat.provider_selector.automatic', 'Automatic'),
+ ('en', 'chat.provider_selector.automatic_hint', 'Let the gateway choose the provider'),
+ ('en', 'chat.provider_selector.col_context', 'Context'),
+ ('en', 'chat.provider_selector.col_latency', 'Latency'),
+ ('en', 'chat.provider_selector.col_price', 'In / Out /M'),
+ ('en', 'chat.provider_selector.col_provider', 'Provider'),
+ ('en', 'chat.provider_selector.col_throughput', 'Throughput'),
+ ('en', 'chat.provider_selector.col_uptime', 'Uptime'),
+ ('en', 'chat.provider_selector.context_tokens', 'Tokens'),
+ ('en', 'chat.provider_selector.expanded_privacy', 'Privacy'),
+ ('en', 'chat.provider_selector.expanded_rate_limits', 'Rate limits'),
+ ('en', 'chat.provider_selector.expanded_tools', 'Tools'),
+ ('en', 'chat.provider_selector.expanded_uptime', 'Uptime'),
+ ('en', 'chat.provider_selector.latency_estimated', 'Estimated'),
+ ('en', 'chat.provider_selector.mode_lock', 'Lock'),
+ ('en', 'chat.provider_selector.mode_prefer', 'Prefer'),
+ ('en', 'chat.provider_selector.no_providers', 'No provider options available'),
+ ('en', 'chat.provider_selector.price_io_label', 'Input / Output'),
+ ('en', 'chat.provider_selector.privacy_on', 'On'),
+ ('en', 'chat.provider_selector.search_placeholder', 'Search providers...'),
+ ('en', 'chat.provider_selector.set_as_default', 'Set as default provider'),
+ ('en', 'chat.provider_selector.sort_label', 'Sort'),
+ ('en', 'chat.provider_selector.sort_latency', 'Latency'),
+ ('en', 'chat.provider_selector.sort_price', 'Price'),
+ ('en', 'chat.provider_selector.sort_recommended', 'Recommended'),
+ ('en', 'chat.provider_selector.sort_throughput', 'Throughput'),
+ ('en', 'chat.provider_selector.sort_uptime', 'Uptime'),
+ ('en', 'chat.provider_selector.status_all_operational', 'All systems operational'),
+ ('en', 'chat.provider_selector.status_degraded', 'Degraded'),
+ ('en', 'chat.provider_selector.status_healthy', 'Healthy'),
+ ('en', 'chat.provider_selector.status_some_issues', 'Some issues detected'),
+ ('en', 'chat.provider_selector.title', 'Provider'),
+ ('en', 'chat.provider_selector.unavailable_for_key', 'This model is disabled for the configured key'),
+ ('en', 'chat.reasoning_selector.disabled', 'Disabled'),
+ ('en', 'chat.reasoning_selector.extra_high', 'Extra High'),
+ ('en', 'chat.reasoning_selector.high', 'High'),
+ ('en', 'chat.reasoning_selector.low', 'Low'),
+ ('en', 'chat.reasoning_selector.medium', 'Medium'),
+ ('en', 'chat.reasoning_selector.minimal', 'Minimal'),
+ ('en', 'chat.reasoning_selector.none', 'None'),
+ ('en', 'chat.reasoning_selector.token_limit', 'Token Limit'),
+ ('en', 'chat.reasoning_selector.tokens', 'Tokens'),
+ ('en', 'chat.streaming_message.assistant', 'Assistant'),
+ ('en', 'chat.tool_execution.arguments', 'Arguments'),
+ ('en', 'chat.tool_execution.complete', 'Complete'),
+ ('en', 'chat.tool_execution.completed_in', 'Completed in {ms}ms'),
+ ('en', 'chat.tool_execution.error', 'Error'),
+ ('en', 'chat.tool_execution.failed', 'Failed'),
+ ('en', 'chat.tool_execution.generated_image', 'Generated Image'),
+ ('en', 'chat.tool_execution.output', 'Output'),
+ ('en', 'chat.tool_execution.running', 'Running...'),
+ ('en', 'chat.tool_execution.tool', 'Tool'),
+ ('en', 'chat.tool_selector.available', 'Available'),
+ ('en', 'chat.tool_selector.clear', 'Clear'),
+ ('en', 'chat.tool_selector.collapse', 'Collapse'),
+ ('en', 'chat.tool_selector.count', '{count} selected'),
+ ('en', 'chat.tool_selector.enabled', 'Enabled'),
+ ('en', 'chat.tool_selector.expand', 'Expand'),
+ ('en', 'chat.tool_selector.no_tool_support', 'Selected model doesn''t support tools'),
+ ('en', 'chat.tool_selector.no_tools', 'No tools available'),
+ ('en', 'chat.tool_selector.no_tools_found', 'No tools found'),
+ ('en', 'chat.tool_selector.no_tools_hint', 'Add an MCP server or a tool to get started.'),
+ ('en', 'chat.tool_selector.save_as_defaults', 'Save as defaults'),
+ ('en', 'chat.tool_selector.search_tools', 'Search tools...'),
+ ('en', 'chat.tool_selector.select_all', 'Select all'),
+ ('en', 'chat.tool_selector.sub_tools', '{count} sub-tools'),
+ ('en', 'chat.tool_selector.title', 'Tools'),
+ ('en', 'chat.tool_selector.tools', 'tools'),
+ ('en', 'chat.tool_selector.unknown_server', 'Unknown MCP server'),
+ ('en', 'common.cancel', 'Cancel'),
+ ('en', 'common.close', 'Close'),
+ ('en', 'common.copied', 'Copied'),
+ ('en', 'common.copy_to_clipboard', 'Copied to clipboard'),
+ ('en', 'common.create', 'Create'),
+ ('en', 'common.delete', 'Delete'),
+ ('en', 'common.error', 'Error'),
+ ('en', 'common.loading', 'Loading...'),
+ ('en', 'common.next', 'Next'),
+ ('en', 'common.previous', 'Previous'),
+ ('en', 'common.refresh', 'Refresh'),
+ ('en', 'common.remove', 'Remove'),
+ ('en', 'common.save', 'Save'),
+ ('en', 'common.user', 'User'),
+ ('en', 'error_messages.theme_import_failed', 'Failed to import theme'),
+ ('en', 'guild.components.dialog_calendar.date.select', 'Select Date'),
+ ('en', 'guild.components.dialog_calendar.ok', 'OK'),
+ ('en', 'guild.components.dialog_calendar.reset', 'Reset'),
+ ('en', 'guild.components.dialog_calendar.time.label', 'Time'),
+ ('en', 'guild.components.dialog_calendar.time.select', 'Select Time'),
+ ('en', 'mcp.add', 'Add server'),
+ ('en', 'mcp.args', 'Arguments (one per line)'),
+ ('en', 'mcp.cancel', 'Cancel'),
+ ('en', 'mcp.command', 'Command'),
+ ('en', 'mcp.create_failed', 'Could not create the MCP server.'),
+ ('en', 'mcp.delete', 'Delete'),
+ ('en', 'mcp.delete_failed', 'Could not delete the MCP server.'),
+ ('en', 'mcp.discover', 'Test & discover tools'),
+ ('en', 'mcp.discover_failed', 'Could not connect to the MCP server.'),
+ ('en', 'mcp.discovered_tools', 'Discovered tools'),
+ ('en', 'mcp.edit', 'Edit'),
+ ('en', 'mcp.enabled', 'Enabled'),
+ ('en', 'mcp.headers', 'Headers (JSON)'),
+ ('en', 'mcp.headers_add', 'Add header'),
+ ('en', 'mcp.headers_empty', 'No headers added yet.'),
+ ('en', 'mcp.headers_name', 'Header name'),
+ ('en', 'mcp.headers_remove', 'Remove header'),
+ ('en', 'mcp.headers_value', 'Header value'),
+ ('en', 'mcp.health', 'Health'),
+ ('en', 'mcp.health_healthy', 'Healthy'),
+ ('en', 'mcp.health_unhealthy', 'Unhealthy'),
+ ('en', 'mcp.health_unknown', 'Unknown'),
+ ('en', 'mcp.manage_description', 'Connect remote Model Context Protocol servers to add tools to your chats.'),
+ ('en', 'mcp.manage_title', 'MCP servers'),
+ ('en', 'mcp.name', 'Name'),
+ ('en', 'mcp.name_placeholder', 'My MCP server'),
+ ('en', 'mcp.no_servers', 'No MCP servers yet.'),
+ ('en', 'mcp.save', 'Save'),
+ ('en', 'mcp.tool_selector.manage', 'Manage MCP servers…'),
+ ('en', 'mcp.transport', 'Transport'),
+ ('en', 'mcp.transport_http', 'Streamable HTTP'),
+ ('en', 'mcp.transport_stdio', 'stdio (server-side)'),
+ ('en', 'mcp.update_failed', 'Could not update the MCP server.'),
+ ('en', 'mcp.url', 'Server URL'),
+ ('en', 'mcp.url_placeholder', 'https://example.com/mcp'),
+ ('en', 'settings.admin.default_model', 'Default model'),
+ ('en', 'settings.admin.default_model_hint', 'Pre-select this model for all users when no team or personal default is set.'),
+ ('en', 'settings.admin_config.coming_soon', 'Configuration management coming soon'),
+ ('en', 'settings.admin_config.description', 'Application configuration settings'),
+ ('en', 'settings.admin_config.provider_selector.hint', 'Let users pick which upstream provider serves each OpenRouter request.'),
+ ('en', 'settings.admin_config.provider_selector.label', 'Chat provider selector'),
('en', 'settings.admin_users.cannot_modify_self', 'You cannot modify your own account'),
- ('de', 'settings.admin_users.cannot_modify_self', 'Sie können Ihr eigenes Konto nicht bearbeiten'),
+ ('en', 'settings.admin_users.coming_soon', 'User management coming soon'),
+ ('en', 'settings.admin_users.create_error', 'Failed to create user'),
+ ('en', 'settings.admin_users.create_success', 'User created'),
+ ('en', 'settings.admin_users.create_user', 'Create User'),
('en', 'settings.admin_users.create_user_description', 'Create a new user with optional roles and password'),
- ('de', 'settings.admin_users.create_user_description', 'Erstellen Sie einen neuen Benutzer mit optionalen Rollen und Passwort'),
+ ('en', 'settings.admin_users.delete_confirm', 'Are you sure you want to delete {username}? This action cannot be undone.'),
+ ('en', 'settings.admin_users.delete_error', 'Failed to delete user'),
+ ('en', 'settings.admin_users.delete_success', 'User deleted'),
+ ('en', 'settings.admin_users.delete_user', 'Delete User'),
+ ('en', 'settings.admin_users.description', 'Manage user accounts and permissions'),
+ ('en', 'settings.admin_users.edit_error', 'Failed to update user'),
+ ('en', 'settings.admin_users.edit_success', 'User updated'),
+ ('en', 'settings.admin_users.edit_user', 'Edit User'),
('en', 'settings.admin_users.field_email', 'Email'),
- ('de', 'settings.admin_users.field_email', 'E-Mail'),
- ('en', 'settings.admin_users.field_username', 'Username'),
- ('de', 'settings.admin_users.field_username', 'Benutzername'),
('en', 'settings.admin_users.field_password', 'Password'),
- ('de', 'settings.admin_users.field_password', 'Passwort'),
('en', 'settings.admin_users.field_roles', 'Roles'),
- ('de', 'settings.admin_users.field_roles', 'Rollen'),
- ('en', 'settings.admin_users.edit_user', 'Edit User'),
- ('de', 'settings.admin_users.edit_user', 'Benutzer bearbeiten'),
+ ('en', 'settings.admin_users.field_teams', 'Teams'),
+ ('en', 'settings.admin_users.field_username', 'Username'),
+ ('en', 'settings.admin_users.filter_role', 'Filter role'),
+ ('en', 'settings.admin_users.filter_team', 'Filter team'),
+ ('en', 'settings.admin_users.load_error', 'Failed to load users'),
+ ('en', 'settings.admin_users.no_users', 'No users found'),
('en', 'settings.admin_users.reset_password', 'Reset Password'),
- ('de', 'settings.admin_users.reset_password', 'Passwort zurücksetzen'),
('en', 'settings.admin_users.reset_password_placeholder', 'New password (leave blank to keep current)'),
- ('de', 'settings.admin_users.reset_password_placeholder', 'Neues Passwort (leer lassen, um aktuelles zu behalten)'),
- ('en', 'settings.admin_users.delete_user', 'Delete User'),
- ('de', 'settings.admin_users.delete_user', 'Benutzer löschen'),
- ('en', 'settings.admin_users.delete_confirm', 'Are you sure you want to delete {username}? This action cannot be undone.'),
- ('de', 'settings.admin_users.delete_confirm', 'Möchten Sie {username} wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.'),
- ('en', 'settings.admin_users.load_error', 'Failed to load users'),
- ('de', 'settings.admin_users.load_error', 'Benutzer konnten nicht geladen werden'),
- ('en', 'settings.admin_users.create_success', 'User created'),
- ('de', 'settings.admin_users.create_success', 'Benutzer erstellt'),
- ('en', 'settings.admin_users.create_error', 'Failed to create user'),
- ('de', 'settings.admin_users.create_error', 'Fehler beim Erstellen des Benutzers'),
- ('en', 'settings.admin_users.edit_success', 'User updated'),
- ('de', 'settings.admin_users.edit_success', 'Benutzer aktualisiert'),
- ('en', 'settings.admin_users.edit_error', 'Failed to update user'),
- ('de', 'settings.admin_users.edit_error', 'Fehler beim Aktualisieren des Benutzers'),
- ('en', 'settings.admin_users.delete_success', 'User deleted'),
- ('de', 'settings.admin_users.delete_success', 'Benutzer gelöscht'),
- ('en', 'settings.admin_users.delete_error', 'Failed to delete user'),
- ('de', 'settings.admin_users.delete_error', 'Fehler beim Löschen des Benutzers'),
+ ('en', 'settings.admin_users.role_all', 'All'),
+ ('en', 'settings.admin_users.search_placeholder', 'Search users...'),
('en', 'settings.admin_users.select_roles', 'Select roles'),
- ('de', 'settings.admin_users.select_roles', 'Rollen auswählen'),
-
- -- models
+ ('en', 'settings.admin_users.select_teams', 'Select teams'),
+ ('en', 'settings.analytics.api_key', 'API key'),
+ ('en', 'settings.analytics.api_keys_soon', 'API key tracking will be available in a future release'),
+ ('en', 'settings.analytics.avg', 'Avg'),
+ ('en', 'settings.analytics.avg_cost', 'Avg. cost / day'),
+ ('en', 'settings.analytics.coming_soon', 'Coming soon'),
+ ('en', 'settings.analytics.cost', 'Cost'),
+ ('en', 'settings.analytics.custom', 'Custom'),
+ ('en', 'settings.analytics.custom_range', 'Custom range'),
+ ('en', 'settings.analytics.daily', 'Daily'),
+ ('en', 'settings.analytics.description', 'Review token and spend trends'),
+ ('en', 'settings.analytics.from', 'From'),
+ ('en', 'settings.analytics.group_by', 'Group by'),
+ ('en', 'settings.analytics.hourly', 'Hourly'),
+ ('en', 'settings.analytics.label', 'Label'),
+ ('en', 'settings.analytics.last_30d', 'Last 30 days'),
+ ('en', 'settings.analytics.last_7d', 'Last 7 days'),
+ ('en', 'settings.analytics.last_90d', 'Last 90 days'),
+ ('en', 'settings.analytics.latency', 'Latency'),
+ ('en', 'settings.analytics.max', 'Max'),
+ ('en', 'settings.analytics.metric', 'Metric'),
+ ('en', 'settings.analytics.min', 'Min'),
+ ('en', 'settings.analytics.model', 'Model'),
+ ('en', 'settings.analytics.my_usage', 'My Usage'),
+ ('en', 'settings.analytics.no_data', 'No data available for this period'),
+ ('en', 'settings.analytics.none', 'None'),
+ ('en', 'settings.analytics.other', 'Other'),
+ ('en', 'settings.analytics.per_day', 'per day'),
+ ('en', 'settings.analytics.preset_last_30d', 'Last 30 days'),
+ ('en', 'settings.analytics.preset_last_7d', 'Last 7 days'),
+ ('en', 'settings.analytics.preset_last_90d', 'Last 90 days'),
+ ('en', 'settings.analytics.preset_last_month', 'Last month'),
+ ('en', 'settings.analytics.preset_this_month', 'This month'),
+ ('en', 'settings.analytics.preset_this_year', 'This year'),
+ ('en', 'settings.analytics.provider', 'Provider'),
+ ('en', 'settings.analytics.reasoning_tokens', 'Reasoning tokens'),
+ ('en', 'settings.analytics.request_count', 'Request count'),
+ ('en', 'settings.analytics.request_volume', 'Request volume by model'),
+ ('en', 'settings.analytics.requests', 'Requests'),
+ ('en', 'settings.analytics.rollup', 'Rollup'),
+ ('en', 'settings.analytics.section_api_keys', 'API Keys'),
+ ('en', 'settings.analytics.section_models', 'Models'),
+ ('en', 'settings.analytics.section_user', 'Users'),
+ ('en', 'settings.analytics.spend', 'Spend'),
+ ('en', 'settings.analytics.spend_over_time', 'Spend over time'),
+ ('en', 'settings.analytics.sum', 'Sum'),
+ ('en', 'settings.analytics.tab_explore', 'Explore'),
+ ('en', 'settings.analytics.tab_overview', 'Overview'),
+ ('en', 'settings.analytics.tab_trends', 'Trends'),
+ ('en', 'settings.analytics.to', 'To'),
+ ('en', 'settings.analytics.token_split', 'Token split'),
+ ('en', 'settings.analytics.tokens', 'Tokens'),
+ ('en', 'settings.analytics.tokens_completion', 'Tokens (completion)'),
+ ('en', 'settings.analytics.tokens_input', 'Tokens (Input)'),
+ ('en', 'settings.analytics.tokens_output', 'Tokens (Output)'),
+ ('en', 'settings.analytics.tokens_prompt', 'Tokens (prompt)'),
+ ('en', 'settings.analytics.tokens_reasoning', 'Tokens (Reasoning)'),
+ ('en', 'settings.analytics.tokens_total', 'Tokens (total)'),
+ ('en', 'settings.analytics.top', 'Top'),
+ ('en', 'settings.analytics.top_api_keys', 'Top API Keys'),
+ ('en', 'settings.analytics.top_models', 'By model'),
+ ('en', 'settings.analytics.top_teams', 'By team'),
+ ('en', 'settings.analytics.top_users', 'By user'),
+ ('en', 'settings.analytics.total', 'Total'),
+ ('en', 'settings.analytics.total_cost', 'Total cost'),
+ ('en', 'settings.analytics.total_tokens', 'Total tokens'),
+ ('en', 'settings.analytics.total_tokens_short', 'Tokens'),
+ ('en', 'settings.analytics.total_usage_dollars', 'Total usage ($)'),
+ ('en', 'settings.analytics.trend', 'Trend'),
+ ('en', 'settings.analytics.trending', 'Trending'),
+ ('en', 'settings.analytics.usage_by_model', 'Usage by model'),
+ ('en', 'settings.analytics.user', 'User'),
+ ('en', 'settings.analytics.value', 'Value'),
+ ('en', 'settings.analytics.weekly', 'Weekly'),
+ ('en', 'settings.appearance.built_in_themes', 'Built-in Themes'),
+ ('en', 'settings.appearance.custom_themes', 'My Themes'),
+ ('en', 'settings.appearance.description', 'Customize the look and feel'),
+ ('en', 'settings.appearance.display_mode', 'Display Mode'),
+ ('en', 'settings.appearance.display_mode_description', 'Choose between light and dark mode'),
+ ('en', 'settings.appearance.import_theme', 'Import Theme'),
+ ('en', 'settings.appearance.language', 'Language'),
+ ('en', 'settings.appearance.language_de', 'German'),
+ ('en', 'settings.appearance.language_description', 'Select your preferred language'),
+ ('en', 'settings.appearance.language_en', 'English'),
+ ('en', 'settings.appearance.loading_themes', 'Loading themes...'),
+ ('en', 'settings.appearance.no_themes_found', 'No themes found'),
+ ('en', 'settings.appearance.random_theme', 'Random theme'),
+ ('en', 'settings.appearance.reset_theme', 'Reset theme'),
+ ('en', 'settings.appearance.search_themes', 'Search themes...'),
+ ('en', 'settings.appearance.theme', 'Theme'),
+ ('en', 'settings.appearance.theme_dark', 'Dark'),
+ ('en', 'settings.appearance.theme_description', 'Choose between light and dark mode'),
+ ('en', 'settings.appearance.theme_light', 'Light'),
+ ('en', 'settings.appearance.theme_system', 'System'),
+ ('en', 'settings.appearance.themes', 'Themes'),
+ ('en', 'settings.appearance.themes_description', 'Select and manage your color themes'),
+ ('en', 'settings.appearance.themes_powered_by', 'Get more themes at'),
+ ('en', 'settings.appearance.title', 'Appearance'),
+ ('en', 'settings.appearance.try_different_search', 'Try adjusting your search query'),
+ ('en', 'settings.back', 'Back'),
+ ('en', 'settings.budgets.affected_users', 'affected'),
+ ('en', 'settings.budgets.all_kinds', 'All kinds'),
+ ('en', 'settings.budgets.amount', 'Amount'),
+ ('en', 'settings.budgets.assign_error', 'Failed to assign budget'),
+ ('en', 'settings.budgets.assign_success', 'Budget assigned'),
+ ('en', 'settings.budgets.assign_team', 'Assign team'),
+ ('en', 'settings.budgets.assign_user', 'Assign user'),
+ ('en', 'settings.budgets.assignment', 'Assignment'),
+ ('en', 'settings.budgets.assignments', 'Assignments'),
+ ('en', 'settings.budgets.budget', 'Budget'),
+ ('en', 'settings.budgets.budget_count', 'budgets'),
+ ('en', 'settings.budgets.create', 'Create budget'),
+ ('en', 'settings.budgets.create_error', 'Failed to create budget'),
+ ('en', 'settings.budgets.create_success', 'Budget created'),
+ ('en', 'settings.budgets.delete_error', 'Failed to delete budget'),
+ ('en', 'settings.budgets.delete_success', 'Budget deleted'),
+ ('en', 'settings.budgets.description', 'Limit spend for users and teams'),
+ ('en', 'settings.budgets.effective_budgets', 'Effective budgets'),
+ ('en', 'settings.budgets.enabled', 'Enabled'),
+ ('en', 'settings.budgets.interval', 'Interval'),
+ ('en', 'settings.budgets.kind', 'Kind'),
+ ('en', 'settings.budgets.kind_scope', 'Kind'),
+ ('en', 'settings.budgets.members', 'members'),
+ ('en', 'settings.budgets.name', 'Name'),
+ ('en', 'settings.budgets.no_assignments', 'No assignments yet'),
+ ('en', 'settings.budgets.no_reset', 'No reset'),
+ ('en', 'settings.budgets.no_reset_history', 'No reset history yet'),
+ ('en', 'settings.budgets.no_team_budgets', 'No team budgets found'),
+ ('en', 'settings.budgets.no_user_budgets', 'No user budgets found'),
+ ('en', 'settings.budgets.notes', 'Description'),
+ ('en', 'settings.budgets.on_exceed', 'Action'),
+ ('en', 'settings.budgets.pick_team', 'Select a team…'),
+ ('en', 'settings.budgets.pick_user', 'Select a user…'),
+ ('en', 'settings.budgets.reached', 'Budget reached'),
+ ('en', 'settings.budgets.reason', 'Reason'),
+ ('en', 'settings.budgets.reason_placeholder', 'Optional note for the audit log'),
+ ('en', 'settings.budgets.remaining', 'Remaining'),
+ ('en', 'settings.budgets.remove_error', 'Failed to remove assignment'),
+ ('en', 'settings.budgets.remove_success', 'Assignment removed'),
+ ('en', 'settings.budgets.reset', 'Reset'),
+ ('en', 'settings.budgets.reset_at', 'Reset at'),
+ ('en', 'settings.budgets.reset_budget', 'Reset budget'),
+ ('en', 'settings.budgets.reset_by', 'Reset by'),
+ ('en', 'settings.budgets.reset_error', 'Failed to reset budget'),
+ ('en', 'settings.budgets.reset_keeps_history', 'Usage history is kept. This starts budget accounting from now for the selected scope.'),
+ ('en', 'settings.budgets.reset_strategy', 'Reset'),
+ ('en', 'settings.budgets.reset_success', 'Budget reset'),
+ ('en', 'settings.budgets.save_error', 'Failed to save budget'),
+ ('en', 'settings.budgets.save_success', 'Budget saved'),
+ ('en', 'settings.budgets.scope', 'Scope'),
+ ('en', 'settings.budgets.search_teams', 'Search teams'),
+ ('en', 'settings.budgets.search_users', 'Search users'),
+ ('en', 'settings.budgets.settings', 'Settings'),
+ ('en', 'settings.budgets.status', 'Status'),
+ ('en', 'settings.budgets.tab_budgets', 'Budgets'),
+ ('en', 'settings.budgets.tab_history', 'History'),
+ ('en', 'settings.budgets.tab_teams', 'Teams'),
+ ('en', 'settings.budgets.tab_users', 'Users'),
+ ('en', 'settings.budgets.team', 'team'),
+ ('en', 'settings.budgets.team_budget', 'team budget'),
+ ('en', 'settings.budgets.teams', 'Teams'),
+ ('en', 'settings.budgets.used', 'Used'),
+ ('en', 'settings.budgets.user', 'User'),
+ ('en', 'settings.budgets.user_budget', 'user budget'),
+ ('en', 'settings.budgets.users_with_budgets', 'users'),
+ ('en', 'settings.description', 'Manage your account preferences and configuration.'),
+ ('en', 'settings.import', 'Import'),
+ ('en', 'settings.import_theme', 'Import Theme'),
+ ('en', 'settings.import_theme_description', 'Paste a theme URL from'),
+ ('en', 'settings.models.active', 'Active'),
('en', 'settings.models.description', 'Manage and configure available AI models.'),
- ('de', 'settings.models.description', 'Verwalte und konfiguriere verfügbare KI-Modelle.'),
- ('en', 'settings.models.search', 'Search models...'),
- ('de', 'settings.models.search', 'Modelle suchen...'),
- ('en', 'settings.models.no_models', 'No models found.'),
- ('de', 'settings.models.no_models', 'Keine Modelle gefunden.'),
- ('en', 'settings.models.not_found', 'Model not found.'),
- ('de', 'settings.models.not_found', 'Modell nicht gefunden.'),
- ('en', 'settings.models.save_success', 'Model configuration saved successfully.'),
- ('de', 'settings.models.save_success', 'Modellkonfiguration erfolgreich gespeichert.'),
- ('en', 'settings.models.save_error', 'Failed to save model configuration.'),
- ('de', 'settings.models.save_error', 'Fehler beim Speichern der Modellkonfiguration.'),
- ('en', 'settings.models.editor.loading', 'Loading model...'),
- ('de', 'settings.models.editor.loading', 'Modell wird geladen...'),
- ('en', 'settings.models.editor.general', 'General Configuration'),
- ('de', 'settings.models.editor.general', 'Allgemeine Konfiguration'),
- ('en', 'settings.models.editor.display_name', 'Display Name'),
- ('de', 'settings.models.editor.display_name', 'Anzeigename'),
- ('en', 'settings.models.editor.is_enabled', 'Enabled'),
- ('de', 'settings.models.editor.is_enabled', 'Aktiviert'),
+ ('en', 'settings.models.disabled', 'Disabled'),
+ ('en', 'settings.models.editor.change_icon', 'Model Icon'),
+ ('en', 'settings.models.editor.characters', 'chars'),
+ ('en', 'settings.models.editor.core_prompt', 'Core Prompt'),
+ ('en', 'settings.models.editor.core_prompt_desc', 'System prompt applied to every conversation using this model.'),
('en', 'settings.models.editor.description', 'Description'),
- ('de', 'settings.models.editor.description', 'Beschreibung'),
- ('en', 'settings.models.editor.icon_url', 'Icon URL'),
- ('de', 'settings.models.editor.icon_url', 'Icon-URL'),
- ('en', 'settings.models.editor.icon_tab_url', 'URL'),
- ('de', 'settings.models.editor.icon_tab_url', 'URL'),
- ('en', 'settings.models.editor.icon_tab_upload', 'Upload'),
- ('de', 'settings.models.editor.icon_tab_upload', 'Hochladen'),
- ('en', 'settings.models.editor.icon_choose_file', 'Choose image'),
- ('de', 'settings.models.editor.icon_choose_file', 'Bild auswählen'),
- ('en', 'settings.models.editor.icon_upload_hint', 'Upload a local image as a model icon.'),
- ('de', 'settings.models.editor.icon_upload_hint', 'Lade ein lokales Bild als Modell-Icon hoch.'),
- ('en', 'settings.models.editor.icon_clear', 'Remove icon'),
- ('de', 'settings.models.editor.icon_clear', 'Icon entfernen'),
- ('en', 'settings.models.editor.system_prompt', 'System Prompt'),
- ('de', 'settings.models.editor.system_prompt', 'System-Prompt'),
- ('en', 'settings.models.editor.system_prompt_desc', 'Base system prompt applied to all conversations with this model.'),
- ('de', 'settings.models.editor.system_prompt_desc', 'Basis-System-Prompt, der auf alle Konversationen mit diesem Modell angewendet wird.'),
- ('en', 'settings.models.editor.system_prompt_placeholder', 'You are a helpful AI assistant...'),
- ('de', 'settings.models.editor.system_prompt_placeholder', 'Du bist ein hilfreicher KI-Assistent...'),
- ('en', 'settings.models.editor.sampling', 'Sampling Parameters'),
- ('de', 'settings.models.editor.sampling', 'Sampling-Parameter'),
- ('en', 'settings.models.editor.sampling_desc', 'Default sampling parameters for this model.'),
- ('de', 'settings.models.editor.sampling_desc', 'Standard-Sampling-Parameter für dieses Modell.'),
- ('en', 'settings.models.editor.temperature', 'Temperature'),
- ('de', 'settings.models.editor.temperature', 'Temperatur'),
- ('en', 'settings.models.editor.top_p', 'Top P'),
- ('de', 'settings.models.editor.top_p', 'Top P'),
- ('en', 'settings.models.editor.max_tokens', 'Max Tokens'),
- ('de', 'settings.models.editor.max_tokens', 'Max. Tokens'),
- ('en', 'settings.models.editor.description_tab_write', 'Write'),
- ('de', 'settings.models.editor.description_tab_write', 'Schreiben'),
- ('en', 'settings.models.editor.description_tab_preview', 'Preview'),
- ('de', 'settings.models.editor.description_tab_preview', 'Vorschau'),
- ('en', 'settings.models.editor.description_placeholder', 'Describe this model using Markdown...'),
- ('de', 'settings.models.editor.description_placeholder', 'Beschreibe dieses Modell mit Markdown...'),
- ('en', 'settings.models.editor.description_markdown_hint', 'Markdown is supported'),
- ('de', 'settings.models.editor.description_markdown_hint', 'Markdown wird unterstützt'),
('en', 'settings.models.editor.description_empty_preview', 'Nothing to preview'),
- ('de', 'settings.models.editor.description_empty_preview', 'Nichts zum Anzeigen'),
- ('en', 'settings.models.editor.unsaved_changes', 'Unsaved changes'),
- ('de', 'settings.models.editor.unsaved_changes', 'Nicht gespeicherte Änderungen'),
- ('en', 'settings.models.editor.unsaved_dialog_title', 'Unsaved Changes'),
- ('de', 'settings.models.editor.unsaved_dialog_title', 'Nicht gespeicherte Änderungen'),
- ('en', 'settings.models.editor.unsaved_dialog_desc', 'You have unsaved changes that will be lost. Are you sure you want to go back?'),
- ('de', 'settings.models.editor.unsaved_dialog_desc', 'Du hast nicht gespeicherte Änderungen, die verloren gehen. Möchtest du wirklich zurückgehen?'),
+ ('en', 'settings.models.editor.description_markdown_hint', 'Markdown is supported'),
+ ('en', 'settings.models.editor.description_placeholder', 'Describe this model using Markdown...'),
+ ('en', 'settings.models.editor.description_tab_preview', 'Preview'),
+ ('en', 'settings.models.editor.description_tab_write', 'Write'),
('en', 'settings.models.editor.discard_changes', 'Discard & Go Back'),
- ('de', 'settings.models.editor.discard_changes', 'Verwerfen & Zurück'),
- ('en', 'settings.models.editor.keep_editing', 'Keep Editing'),
- ('de', 'settings.models.editor.keep_editing', 'Weiter bearbeiten'),
-
- -- Model editor – new keys (EN)
- ('en', 'settings.models.editor.general_info', 'General Info'),
- ('en', 'settings.models.editor.general_info_desc', 'Basic model identity and availability settings.'),
- ('en', 'settings.models.editor.model_id', 'Model ID'),
+ ('en', 'settings.models.editor.display_name', 'Display Name'),
+ ('en', 'settings.models.editor.display_name_placeholder', 'e.g. GPT-4o'),
+ ('en', 'settings.models.editor.effective_price', 'Effective price'),
('en', 'settings.models.editor.enable_model', 'Enable Model'),
('en', 'settings.models.editor.enable_model_desc', 'Make this model available to users.'),
- ('en', 'settings.models.editor.change_icon', 'Model Icon'),
- ('en', 'settings.models.editor.upload', 'Upload'),
- ('en', 'settings.models.editor.icon_url_placeholder', 'https://example.com/icon.png'),
- ('en', 'settings.models.editor.display_name_placeholder', 'e.g. GPT-4o'),
- ('en', 'settings.models.editor.core_prompt', 'Core Prompt'),
- ('en', 'settings.models.editor.core_prompt_desc', 'System prompt applied to every conversation using this model.'),
- ('en', 'settings.models.editor.system_prompt_hint', 'Variables like {{user_name}} are interpolated at runtime.'),
- ('en', 'settings.models.editor.characters', 'chars'),
- ('en', 'settings.models.editor.parameters', 'Parameters'),
- ('en', 'settings.models.editor.parameters_desc', 'Default sampling parameters. Only enabled parameters are sent to the model; disabled ones use provider defaults.'),
- ('en', 'settings.models.editor.temperature_tooltip', 'Controls randomness. Lower values produce more deterministic output; higher values increase creativity.'),
- ('en', 'settings.models.editor.temperature_precise', 'precise ← → creative'),
- ('en', 'settings.models.editor.max_tokens_tooltip', 'Maximum number of tokens to generate in the response.'),
- ('en', 'settings.models.editor.top_p_tooltip', 'Nucleus sampling threshold. Only tokens whose cumulative probability mass reaches this value are considered.'),
- ('en', 'settings.models.editor.top_k_tooltip', 'Limits the next token choice to the K most probable tokens. 0 disables this filter.'),
('en', 'settings.models.editor.frequency_penalty_tooltip', 'Penalises tokens that have already appeared frequently. Positive values reduce repetition; negative values encourage it.'),
- ('en', 'settings.models.editor.presence_penalty_tooltip', 'Penalises tokens that have appeared at all. Encourages the model to discuss new topics.'),
- ('en', 'settings.models.editor.seed', 'Seed'),
- ('en', 'settings.models.editor.seed_tooltip', 'Random seed for deterministic generation. Same seed with same prompt should produce the same output.'),
- ('en', 'settings.models.editor.seed_placeholder', 'e.g. 42'),
- ('en', 'settings.models.editor.reasoning_effort', 'Reasoning Effort'),
- ('en', 'settings.models.editor.reasoning_effort_tooltip', 'Controls how much reasoning the model performs before generating a response.'),
- ('en', 'settings.models.editor.reasoning_effort_placeholder', 'Select effort level'),
- ('en', 'settings.models.editor.reasoning_budget', 'Reasoning Token Budget'),
- ('en', 'settings.models.editor.reasoning_budget_tooltip', 'Maximum number of tokens the model may spend on internal reasoning before generating the response.'),
- ('en', 'settings.models.editor.stop_sequences', 'Stop Sequences'),
- ('en', 'settings.models.editor.stop_sequences_tooltip', 'The model stops generating when it produces one of these sequences. Useful for structured output.'),
- ('en', 'settings.models.editor.stop_sequences_placeholder', 'Enter a stop string and press Enter'),
+ ('en', 'settings.models.editor.general', 'General Configuration'),
+ ('en', 'settings.models.editor.general_info', 'General Info'),
+ ('en', 'settings.models.editor.general_info_desc', 'Basic model identity and availability settings.'),
+ ('en', 'settings.models.editor.icon_choose_file', 'Choose image'),
+ ('en', 'settings.models.editor.icon_clear', 'Remove icon'),
+ ('en', 'settings.models.editor.icon_tab_upload', 'Upload'),
+ ('en', 'settings.models.editor.icon_tab_url', 'URL'),
+ ('en', 'settings.models.editor.icon_upload_hint', 'Upload a local image as a model icon.'),
+ ('en', 'settings.models.editor.icon_url', 'Icon URL'),
+ ('en', 'settings.models.editor.icon_url_placeholder', 'https://example.com/icon.png'),
+ ('en', 'settings.models.editor.is_enabled', 'Enabled'),
+ ('en', 'settings.models.editor.keep_editing', 'Keep Editing'),
+ ('en', 'settings.models.editor.loading', 'Loading model...'),
('en', 'settings.models.editor.logprobs', 'Log Probabilities'),
- ('en', 'settings.models.editor.logprobs_tooltip', 'Return the log probabilities of the output tokens.'),
('en', 'settings.models.editor.logprobs_enable', 'Return log probabilities'),
- ('en', 'settings.models.editor.top_logprobs', 'Top Logprobs'),
- ('en', 'settings.models.editor.top_logprobs_tooltip', 'Number of most likely tokens to return at each position, alongside their log probabilities.'),
+ ('en', 'settings.models.editor.logprobs_tooltip', 'Return the log probabilities of the output tokens.'),
+ ('en', 'settings.models.editor.max_tokens', 'Max Tokens'),
+ ('en', 'settings.models.editor.max_tokens_tooltip', 'Maximum number of tokens to generate in the response.'),
+ ('en', 'settings.models.editor.model_id', 'Model ID'),
+ ('en', 'settings.models.editor.override_cache_read', 'Cache read override'),
+ ('en', 'settings.models.editor.override_cache_write', 'Cache write override'),
+ ('en', 'settings.models.editor.override_input', 'Input override'),
+ ('en', 'settings.models.editor.override_output', 'Output override'),
+ ('en', 'settings.models.editor.override_reasoning', 'Reasoning override'),
('en', 'settings.models.editor.parallel_tool_calls', 'Parallel Tool Calls'),
- ('en', 'settings.models.editor.parallel_tool_calls_tooltip', 'Allow the model to invoke multiple tools in parallel within a single turn.'),
('en', 'settings.models.editor.parallel_tool_calls_enable', 'Enable parallel tool calls'),
+ ('en', 'settings.models.editor.parallel_tool_calls_tooltip', 'Allow the model to invoke multiple tools in parallel within a single turn.'),
+ ('en', 'settings.models.editor.param_group_advanced', 'Advanced'),
('en', 'settings.models.editor.param_group_core', 'Core'),
- ('en', 'settings.models.editor.param_group_token', 'Token Sampling'),
('en', 'settings.models.editor.param_group_penalties', 'Penalties'),
('en', 'settings.models.editor.param_group_reasoning', 'Reasoning'),
- ('en', 'settings.models.editor.param_group_advanced', 'Advanced'),
- ('en', 'settings.models.editor.temperature_label_low', 'precise'),
+ ('en', 'settings.models.editor.param_group_token', 'Token Sampling'),
+ ('en', 'settings.models.editor.parameters', 'Parameters'),
+ ('en', 'settings.models.editor.parameters_desc', 'Default sampling parameters. Only enabled parameters are sent to the model; disabled ones use provider defaults.'),
+ ('en', 'settings.models.editor.presence_penalty_tooltip', 'Penalises tokens that have appeared at all. Encourages the model to discuss new topics.'),
+ ('en', 'settings.models.editor.pricing', 'Pricing'),
+ ('en', 'settings.models.editor.pricing_deleted', 'Pricing override removed'),
+ ('en', 'settings.models.editor.pricing_desc', 'Override accounting prices per one million tokens.'),
+ ('en', 'settings.models.editor.pricing_saved', 'Pricing saved'),
+ ('en', 'settings.models.editor.pricing_unit', 'Rates are per one million tokens. Leave optional fields empty to keep them unset.'),
+ ('en', 'settings.models.editor.provider_col_context', 'Context'),
+ ('en', 'settings.models.editor.provider_col_latency', 'Latency'),
+ ('en', 'settings.models.editor.provider_col_price', 'Price (in/out per 1M)'),
+ ('en', 'settings.models.editor.provider_col_provider', 'Provider'),
+ ('en', 'settings.models.editor.provider_col_quant', 'Quantization'),
+ ('en', 'settings.models.editor.provider_col_status', 'Status'),
+ ('en', 'settings.models.editor.provider_col_throughput', 'Throughput'),
+ ('en', 'settings.models.editor.provider_col_uptime', 'Uptime'),
+ ('en', 'settings.models.editor.providers', 'Providers'),
+ ('en', 'settings.models.editor.providers_desc', 'Provider endpoints that can serve this model, with status and pricing.'),
+ ('en', 'settings.models.editor.providers_empty', 'No provider options available'),
+ ('en', 'settings.models.editor.providers_load_error', 'Failed to load provider options'),
+ ('en', 'settings.models.editor.providers_refresh', 'Refresh'),
+ ('en', 'settings.models.editor.providers_user_unavailable', 'This model is disabled for the configured API key.'),
+ ('en', 'settings.models.editor.reasoning_budget', 'Reasoning Token Budget'),
+ ('en', 'settings.models.editor.reasoning_budget_tooltip', 'Maximum number of tokens the model may spend on internal reasoning before generating the response.'),
+ ('en', 'settings.models.editor.reasoning_effort', 'Reasoning Effort'),
+ ('en', 'settings.models.editor.reasoning_effort_placeholder', 'Select effort level'),
+ ('en', 'settings.models.editor.reasoning_effort_tooltip', 'Controls how much reasoning the model performs before generating a response.'),
+ ('en', 'settings.models.editor.reported_price', 'Reported price'),
+ ('en', 'settings.models.editor.row_available', 'Available'),
+ ('en', 'settings.models.editor.row_unavailable', 'Unavailable'),
+ ('en', 'settings.models.editor.row_unknown', 'Unknown'),
+ ('en', 'settings.models.editor.row_user_unavailable', 'Disabled for this key'),
+ ('en', 'settings.models.editor.sampling', 'Sampling Parameters'),
+ ('en', 'settings.models.editor.sampling_desc', 'Default sampling parameters for this model.'),
+ ('en', 'settings.models.editor.seed', 'Seed'),
+ ('en', 'settings.models.editor.seed_placeholder', 'e.g. 42'),
+ ('en', 'settings.models.editor.seed_tooltip', 'Random seed for deterministic generation. Same seed with same prompt should produce the same output.'),
+ ('en', 'settings.models.editor.stop_sequences', 'Stop Sequences'),
+ ('en', 'settings.models.editor.stop_sequences_placeholder', 'Enter a stop string and press Enter'),
+ ('en', 'settings.models.editor.stop_sequences_tooltip', 'The model stops generating when it produces one of these sequences. Useful for structured output.'),
+ ('en', 'settings.models.editor.system_prompt', 'System Prompt'),
+ ('en', 'settings.models.editor.system_prompt_desc', 'Base system prompt applied to all conversations with this model.'),
+ ('en', 'settings.models.editor.system_prompt_hint', 'Variables like {{user_name}} are interpolated at runtime.'),
+ ('en', 'settings.models.editor.system_prompt_placeholder', 'You are a helpful AI assistant...'),
+ ('en', 'settings.models.editor.temperature', 'Temperature'),
('en', 'settings.models.editor.temperature_label_high', 'creative'),
- ('en', 'common.remove', 'Remove'),
- ('en', 'common.copied', 'Copied'),
-
- -- Model editor – new keys (DE)
- ('de', 'settings.models.editor.general_info', 'Allgemeine Informationen'),
- ('de', 'settings.models.editor.general_info_desc', 'Grundlegende Identitäts- und Verfügbarkeitseinstellungen.'),
- ('de', 'settings.models.editor.model_id', 'Modell-ID'),
- ('de', 'settings.models.editor.enable_model', 'Modell aktivieren'),
- ('de', 'settings.models.editor.enable_model_desc', 'Dieses Modell für Benutzer verfügbar machen.'),
- ('de', 'settings.models.editor.change_icon', 'Modell-Icon'),
- ('de', 'settings.models.editor.upload', 'Hochladen'),
- ('de', 'settings.models.editor.icon_url_placeholder', 'https://example.com/icon.png'),
- ('de', 'settings.models.editor.display_name_placeholder', 'z.B. GPT-4o'),
- ('de', 'settings.models.editor.core_prompt', 'System-Prompt'),
- ('de', 'settings.models.editor.core_prompt_desc', 'System-Prompt, der bei jeder Unterhaltung mit diesem Modell verwendet wird.'),
- ('de', 'settings.models.editor.system_prompt_hint', 'Variablen wie {{user_name}} werden zur Laufzeit ersetzt.'),
- ('de', 'settings.models.editor.characters', 'Zeichen'),
- ('de', 'settings.models.editor.parameters', 'Parameter'),
- ('de', 'settings.models.editor.parameters_desc', 'Standard-Sampling-Parameter. Nur aktivierte Parameter werden an das Modell gesendet; deaktivierte nutzen die Anbieter-Standardwerte.'),
- ('de', 'settings.models.editor.temperature_tooltip', 'Steuert die Zufälligkeit. Niedrigere Werte erzeugen deterministischere Ausgaben; höhere Werte erhöhen die Kreativität.'),
- ('de', 'settings.models.editor.temperature_precise', 'präzise ← → kreativ'),
- ('de', 'settings.models.editor.max_tokens_tooltip', 'Maximale Anzahl der zu generierenden Tokens in der Antwort.'),
- ('de', 'settings.models.editor.top_p_tooltip', 'Nucleus-Sampling-Schwellenwert. Es werden nur Tokens berücksichtigt, deren kumulierte Wahrscheinlichkeitsmasse diesen Wert erreicht.'),
- ('de', 'settings.models.editor.top_k_tooltip', 'Begrenzt die Tokenauswahl auf die K wahrscheinlichsten Tokens. 0 deaktiviert diesen Filter.'),
- ('de', 'settings.models.editor.frequency_penalty_tooltip', 'Bestraft häufig vorgekommene Tokens. Positive Werte reduzieren Wiederholungen; negative Werte fördern sie.'),
- ('de', 'settings.models.editor.presence_penalty_tooltip', 'Bestraft Tokens, die bereits erschienen sind. Ermutigt das Modell, neue Themen zu besprechen.'),
- ('de', 'settings.models.editor.seed', 'Seed'),
- ('de', 'settings.models.editor.seed_tooltip', 'Zufalls-Seed für deterministische Generierung. Gleicher Seed mit gleichem Prompt sollte dieselbe Ausgabe erzeugen.'),
- ('de', 'settings.models.editor.seed_placeholder', 'z.B. 42'),
- ('de', 'settings.models.editor.reasoning_effort', 'Reasoning-Aufwand'),
- ('de', 'settings.models.editor.reasoning_effort_tooltip', 'Steuert, wie viel Reasoning das Modell vor der Antwortgenerierung durchführt.'),
- ('de', 'settings.models.editor.reasoning_effort_placeholder', 'Aufwandsstufe auswählen'),
- ('de', 'settings.models.editor.reasoning_budget', 'Reasoning-Token-Budget'),
- ('de', 'settings.models.editor.reasoning_budget_tooltip', 'Maximale Anzahl von Tokens, die das Modell für internes Reasoning verwenden darf.'),
- ('de', 'settings.models.editor.stop_sequences', 'Stop-Sequenzen'),
- ('de', 'settings.models.editor.stop_sequences_tooltip', 'Das Modell stoppt die Generierung, wenn es eine dieser Sequenzen erzeugt.'),
- ('de', 'settings.models.editor.stop_sequences_placeholder', 'Stop-String eingeben und Enter drücken'),
- ('de', 'settings.models.editor.logprobs', 'Log-Wahrscheinlichkeiten'),
- ('de', 'settings.models.editor.logprobs_tooltip', 'Gibt die Log-Wahrscheinlichkeiten der Ausgabe-Tokens zurück.'),
- ('de', 'settings.models.editor.logprobs_enable', 'Log-Wahrscheinlichkeiten zurückgeben'),
- ('de', 'settings.models.editor.top_logprobs', 'Top Log-Wahrscheinlichkeiten'),
- ('de', 'settings.models.editor.top_logprobs_tooltip', 'Anzahl der wahrscheinlichsten Tokens je Position, zusammen mit ihren Log-Wahrscheinlichkeiten.'),
- ('de', 'settings.models.editor.parallel_tool_calls', 'Parallele Tool-Aufrufe'),
- ('de', 'settings.models.editor.parallel_tool_calls_tooltip', 'Erlaubt dem Modell, mehrere Tools gleichzeitig in einem Zug aufzurufen.'),
- ('de', 'settings.models.editor.parallel_tool_calls_enable', 'Parallele Tool-Aufrufe aktivieren'),
- ('de', 'settings.models.editor.param_group_core', 'Kern'),
- ('de', 'settings.models.editor.param_group_token', 'Token-Sampling'),
- ('de', 'settings.models.editor.param_group_penalties', 'Strafen'),
- ('de', 'settings.models.editor.param_group_reasoning', 'Reasoning'),
- ('de', 'settings.models.editor.param_group_advanced', 'Erweitert'),
- ('de', 'settings.models.editor.temperature_label_low', 'präzise'),
- ('de', 'settings.models.editor.temperature_label_high', 'kreativ'),
- ('de', 'common.remove', 'Entfernen'),
- ('de', 'common.copied', 'Kopiert'),
-
- ('en', 'settings.models.editor.variables_panel_title', 'Available Variables'),
- ('en', 'settings.models.editor.variables_panel_desc', 'These placeholders are replaced at runtime when the system prompt is sent to the model.'),
- ('en', 'settings.models.editor.var_user_name', 'Username of the logged-in user'),
- ('en', 'settings.models.editor.var_user_email', 'Email address of the logged-in user'),
+ ('en', 'settings.models.editor.temperature_label_low', 'precise'),
+ ('en', 'settings.models.editor.temperature_precise', 'precise ← → creative'),
+ ('en', 'settings.models.editor.temperature_tooltip', 'Controls randomness. Lower values produce more deterministic output; higher values increase creativity.'),
+ ('en', 'settings.models.editor.top_k_tooltip', 'Limits the next token choice to the K most probable tokens. 0 disables this filter.'),
+ ('en', 'settings.models.editor.top_logprobs', 'Top Logprobs'),
+ ('en', 'settings.models.editor.top_logprobs_tooltip', 'Number of most likely tokens to return at each position, alongside their log probabilities.'),
+ ('en', 'settings.models.editor.top_p', 'Top P'),
+ ('en', 'settings.models.editor.top_p_tooltip', 'Nucleus sampling threshold. Only tokens whose cumulative probability mass reaches this value are considered.'),
+ ('en', 'settings.models.editor.unsaved_changes', 'Unsaved changes'),
+ ('en', 'settings.models.editor.unsaved_dialog_desc', 'You have unsaved changes that will be lost. Are you sure you want to go back?'),
+ ('en', 'settings.models.editor.unsaved_dialog_title', 'Unsaved Changes'),
+ ('en', 'settings.models.editor.upload', 'Upload'),
('en', 'settings.models.editor.var_date', 'Current UTC date (YYYY-MM-DD)'),
- ('en', 'settings.models.editor.var_time', 'Current UTC time (HH:MM)'),
('en', 'settings.models.editor.var_datetime', 'Current UTC date and time (YYYY-MM-DD HH:MM)'),
- ('en', 'settings.models.editor.var_model_name', 'Display name of the model'),
('en', 'settings.models.editor.var_model_id', 'Identifier string of the model'),
-
- ('de', 'settings.models.editor.variables_panel_title', 'Verfügbare Variablen'),
- ('de', 'settings.models.editor.variables_panel_desc', 'Diese Platzhalter werden beim Senden des System-Prompts an das Modell ersetzt.'),
- ('de', 'settings.models.editor.var_user_name', 'Benutzername des eingeloggten Nutzers'),
- ('de', 'settings.models.editor.var_user_email', 'E-Mail-Adresse des eingeloggten Nutzers'),
- ('de', 'settings.models.editor.var_date', 'Aktuelles UTC-Datum (JJJJ-MM-TT)'),
- ('de', 'settings.models.editor.var_time', 'Aktuelle UTC-Zeit (HH:MM)'),
- ('de', 'settings.models.editor.var_datetime', 'Aktuelles UTC-Datum und Uhrzeit (JJJJ-MM-TT HH:MM)'),
- ('de', 'settings.models.editor.var_model_name', 'Anzeigename des Modells'),
- ('de', 'settings.models.editor.var_model_id', 'Bezeichner-String des Modells'),
- ('de', 'settings.models.disabled', 'Deaktiviert'),
- ('de', 'settings.models.active', 'Aktiviert'),
- ('en', 'settings.models.disabled', 'Disabled'),
- ('en', 'settings.models.active', 'Active')
-
+ ('en', 'settings.models.editor.var_model_name', 'Display name of the model'),
+ ('en', 'settings.models.editor.var_time', 'Current UTC time (HH:MM)'),
+ ('en', 'settings.models.editor.var_user_email', 'Email address of the logged-in user'),
+ ('en', 'settings.models.editor.var_user_name', 'Username of the logged-in user'),
+ ('en', 'settings.models.editor.variables_panel_desc', 'These placeholders are replaced at runtime when the system prompt is sent to the model.'),
+ ('en', 'settings.models.editor.variables_panel_title', 'Available Variables'),
+ ('en', 'settings.models.no_models', 'No models found.'),
+ ('en', 'settings.models.not_found', 'Model not found.'),
+ ('en', 'settings.models.save_error', 'Failed to save model configuration.'),
+ ('en', 'settings.models.save_success', 'Model configuration saved successfully.'),
+ ('en', 'settings.models.search', 'Search models...'),
+ ('en', 'settings.profile.chat_defaults', 'Chat Defaults'),
+ ('en', 'settings.profile.chat_defaults_description', 'Pre-configure model, provider, and tools for every new chat.'),
+ ('en', 'settings.profile.default_model', 'Default model'),
+ ('en', 'settings.profile.default_model_hint', 'Pre-select this model when starting a new chat. Overrides team and global defaults.'),
+ ('en', 'settings.profile.default_provider', 'Default Provider'),
+ ('en', 'settings.profile.default_provider_hint', 'Pre-select an upstream provider for the default model. Only visible for gateway models.'),
+ ('en', 'settings.profile.default_provider_placeholder', 'Automatic'),
+ ('en', 'settings.profile.default_tools', 'Default Tools'),
+ ('en', 'settings.profile.default_tools_hint', 'These tools will be pre-enabled when starting a new chat.'),
+ ('en', 'settings.profile.description', 'Your personal account information'),
+ ('en', 'settings.profile.edit', 'Edit'),
+ ('en', 'settings.profile.email', 'Email'),
+ ('en', 'settings.profile.name', 'Name'),
+ ('en', 'settings.profile.no_tools_available', 'No tools available. Connect an MCP server or add tools in the Tools settings.'),
+ ('en', 'settings.profile.save_error', 'Failed to save chat defaults.'),
+ ('en', 'settings.profile.save_success', 'Chat defaults saved.'),
+ ('en', 'settings.profile.title', 'Profile Information'),
+ ('en', 'settings.profile.use_higher_default', 'Use team / global default'),
+ ('en', 'settings.profile.user_id', 'User ID'),
+ ('en', 'settings.providers.add_custom', 'Add Custom Provider'),
+ ('en', 'settings.providers.anthropic_description', 'Access Opus 4.5, Sonnet 4.5 and other Anthropic models'),
+ ('en', 'settings.providers.api_key', 'API Key'),
+ ('en', 'settings.providers.api_key_hint', 'Your API key is stored securely. If encryption is configured, it is encrypted at rest'),
+ ('en', 'settings.providers.api_key_placeholder', 'Enter your API key'),
+ ('en', 'settings.providers.base_url', 'Base URL'),
+ ('en', 'settings.providers.billing.admin_key', 'OpenAI Admin API key'),
+ ('en', 'settings.providers.billing.admin_key_hint', 'A separate organization Admin API key used only to read project costs.'),
+ ('en', 'settings.providers.billing.credits_remaining', '{amount} credits remaining'),
+ ('en', 'settings.providers.billing.enable', 'Enable upstream billing'),
+ ('en', 'settings.providers.billing.encryption_required', 'Set ENCRYPTION_KEY before storing billing credentials.'),
+ ('en', 'settings.providers.billing.failed', 'Last update failed'),
+ ('en', 'settings.providers.billing.key_limit', 'Key limit: {amount}'),
+ ('en', 'settings.providers.billing.local_month', '{amount} this month'),
+ ('en', 'settings.providers.billing.local_only', 'OxideChat tracks local spend. This provider does not have a supported upstream billing integration.'),
+ ('en', 'settings.providers.billing.local_tracked', 'OxideChat tracked'),
+ ('en', 'settings.providers.billing.management_key', 'OpenRouter management key (optional)'),
+ ('en', 'settings.providers.billing.management_key_hint', 'Use an account management key for account-wide purchased and used credits. Without one, the inference key reports key-level usage.'),
+ ('en', 'settings.providers.billing.never_updated', 'Not refreshed yet'),
+ ('en', 'settings.providers.billing.over_threshold', 'Over threshold by {amount}'),
+ ('en', 'settings.providers.billing.period', 'Period'),
+ ('en', 'settings.providers.billing.project_id', 'OpenAI project ID'),
+ ('en', 'settings.providers.billing.project_name', 'Project display name (optional)'),
+ ('en', 'settings.providers.billing.provider_reported', 'Provider reported'),
+ ('en', 'settings.providers.billing.refresh', 'Refresh'),
+ ('en', 'settings.providers.billing.refresh_failed', 'Billing refresh failed'),
+ ('en', 'settings.providers.billing.remaining_threshold', '{amount} remaining to the {threshold} threshold'),
+ ('en', 'settings.providers.billing.remove', 'Remove billing connection'),
+ ('en', 'settings.providers.billing.removed', 'Billing connection removed'),
+ ('en', 'settings.providers.billing.saved', 'Billing settings saved'),
+ ('en', 'settings.providers.billing.setup_required', 'Billing setup required'),
+ ('en', 'settings.providers.billing.spent_month', '{amount} spent this month'),
+ ('en', 'settings.providers.billing.stale', 'Stale'),
+ ('en', 'settings.providers.billing.tab', 'Billing'),
+ ('en', 'settings.providers.billing.threshold', 'Spend threshold'),
+ ('en', 'settings.providers.billing.title', 'Billing overview'),
+ ('en', 'settings.providers.billing.unsupported', 'Unsupported by provider'),
+ ('en', 'settings.providers.billing.updated', 'Updated {time}'),
+ ('en', 'settings.providers.catalog_available', 'Available'),
+ ('en', 'settings.providers.catalog_disabled_key', 'Disabled for this key'),
+ ('en', 'settings.providers.catalog_empty', 'No catalog models found'),
+ ('en', 'settings.providers.catalog_search', 'Search catalog models...'),
+ ('en', 'settings.providers.configure', 'Configure'),
+ ('en', 'settings.providers.configure_description', 'Enter your API credentials to enable this provider'),
+ ('en', 'settings.providers.configured', 'Configured'),
+ ('en', 'settings.providers.delete_error', 'Failed to remove provider'),
+ ('en', 'settings.providers.delete_success', 'Provider removed successfully'),
+ ('en', 'settings.providers.description', 'Bring your own API keys for enhanced AI capabilities'),
+ ('en', 'settings.providers.edit', 'Configure'),
+ ('en', 'settings.providers.enabled', 'Enable Provider'),
+ ('en', 'settings.providers.enabled_hint', 'Allow this provider to be used for AI requests'),
+ ('en', 'settings.providers.google_description', 'Access Gemini 3 family and other Google AI models'),
+ ('en', 'settings.providers.name', 'Display Name'),
+ ('en', 'settings.providers.openai_description', 'Access GPT-5.2, GPT-5.2-Codex and other OpenAI models'),
+ ('en', 'settings.providers.openrouter_description', 'Access a wide variety of models through OpenRouter'),
+ ('en', 'settings.providers.save_error', 'Failed to save provider configuration'),
+ ('en', 'settings.providers.save_success', 'Provider configuration saved successfully'),
+ ('en', 'settings.providers.syncing_provider', 'Syncing models'),
+ ('en', 'settings.providers.syncing_provider_description', 'Models for this provider are being synced, this may take a moment'),
+ ('en', 'settings.providers.syncing_provider_success', 'Models successfully synced'),
+ ('en', 'settings.providers.tab_catalog', 'Catalog'),
+ ('en', 'settings.providers.tab_settings', 'Settings'),
+ ('en', 'settings.providers.title', 'BYOK AI Providers'),
+ ('en', 'settings.providers.toggling_provider', 'Toggling provider'),
+ ('en', 'settings.providers.toggling_provider_description', 'Please wait while the provider is being toggled'),
+ ('en', 'settings.providers.toggling_provider_success', 'Provider successfully toggled'),
+ ('en', 'settings.schema_builder.add_property', 'Add Property'),
+ ('en', 'settings.schema_builder.default', 'Default'),
+ ('en', 'settings.schema_builder.default_placeholder', 'Default value'),
+ ('en', 'settings.schema_builder.description', 'Description'),
+ ('en', 'settings.schema_builder.description_placeholder', 'Describe this parameter'),
+ ('en', 'settings.schema_builder.json_format', 'JSON Schema format'),
+ ('en', 'settings.schema_builder.options', 'Options (for dropdown)'),
+ ('en', 'settings.schema_builder.options_hint', 'Comma-separated. Leave empty for free-form input.'),
+ ('en', 'settings.schema_builder.options_placeholder', 'option1, option2, option3'),
+ ('en', 'settings.schema_builder.required', 'Required'),
+ ('en', 'settings.schema_builder.secret', 'Secret (e.g. API key)'),
+ ('en', 'settings.schema_builder.type', 'Type'),
+ ('en', 'settings.sessions.current', 'Current'),
+ ('en', 'settings.sessions.description', 'Manage your active login sessions across devices'),
+ ('en', 'settings.sessions.last_seen', 'Last seen'),
+ ('en', 'settings.sessions.sign_out', 'Sign Out'),
+ ('en', 'settings.sessions.title', 'Active Sessions'),
+ ('en', 'settings.tabs.admin_config', 'Configuration'),
+ ('en', 'settings.tabs.admin_users', 'Users'),
+ ('en', 'settings.tabs.ai_options', 'AI Options'),
+ ('en', 'settings.tabs.analytics', 'Analytics'),
+ ('en', 'settings.tabs.appearance', 'Appearance'),
+ ('en', 'settings.tabs.budgets', 'Budgets'),
+ ('en', 'settings.tabs.models', 'Models'),
+ ('en', 'settings.tabs.profile', 'Profile'),
+ ('en', 'settings.tabs.providers', 'Providers'),
+ ('en', 'settings.tabs.teams', 'Teams'),
+ ('en', 'settings.tabs.tools', 'Tools'),
+ ('en', 'settings.tabs.usage_analytics', 'Usage Analytics'),
+ ('en', 'settings.teams.allow_all_models', 'Allow all models'),
+ ('en', 'settings.teams.allow_all_models_hint', 'Members can use every enabled model. Turn off to restrict access.'),
+ ('en', 'settings.teams.budget', 'Budget'),
+ ('en', 'settings.teams.budget_link_hint', 'Link this team to a budget configuration to track and enforce usage limits.'),
+ ('en', 'settings.teams.budget_link_title', 'Budget configuration'),
+ ('en', 'settings.teams.budget_linked', 'Linked budget'),
+ ('en', 'settings.teams.budget_none_available', 'No budget configurations exist yet. Once created, you can link one here.'),
+ ('en', 'settings.teams.budget_not_linked', 'No budget linked'),
+ ('en', 'settings.teams.budget_unlink', 'Unlink'),
+ ('en', 'settings.teams.confirm_delete', 'Delete this team? Members keep access through their other teams.'),
+ ('en', 'settings.teams.create', 'Create Team'),
+ ('en', 'settings.teams.create_error', 'Failed to create team'),
+ ('en', 'settings.teams.create_success', 'Team created'),
+ ('en', 'settings.teams.default', 'Default'),
+ ('en', 'settings.teams.default_model', 'Default model'),
+ ('en', 'settings.teams.default_model_hint', 'Pre-select this model for team members. Overrides the global default.'),
+ ('en', 'settings.teams.delete_error', 'Failed to delete team'),
+ ('en', 'settings.teams.delete_success', 'Team deleted'),
+ ('en', 'settings.teams.description', 'Organize users, model access, and budgets'),
+ ('en', 'settings.teams.description_field', 'Description'),
+ ('en', 'settings.teams.load_error', 'Failed to load teams'),
+ ('en', 'settings.teams.members', 'Members'),
+ ('en', 'settings.teams.members_count', '{count} members'),
+ ('en', 'settings.teams.models', 'Models'),
+ ('en', 'settings.teams.name', 'Name'),
+ ('en', 'settings.teams.no_members', 'No users found'),
+ ('en', 'settings.teams.no_models', 'No models found'),
+ ('en', 'settings.teams.no_selection', 'Select a team to manage its members, models, and budget.'),
+ ('en', 'settings.teams.no_selection_title', 'No team selected'),
+ ('en', 'settings.teams.providers', 'Providers'),
+ ('en', 'settings.teams.save_error', 'Failed to save team'),
+ ('en', 'settings.teams.save_success', 'Team saved'),
+ ('en', 'settings.teams.search', 'Search teams...'),
+ ('en', 'settings.teams.search_members', 'Search members...'),
+ ('en', 'settings.teams.search_models', 'Search models...'),
+ ('en', 'settings.teams.selected_count', '{count} selected'),
+ ('en', 'settings.teams.use_global_default', 'Use global default'),
+ ('en', 'settings.theme_deleted', 'Theme deleted'),
+ ('en', 'settings.theme_imported', 'Theme imported successfully'),
+ ('en', 'settings.theme_saved', 'Theme preferences saved'),
+ ('en', 'settings.theme_url', 'Theme URL'),
+ ('en', 'settings.title', 'Settings'),
+ ('en', 'settings.tool_test.array_hint', 'Enter values separated by commas'),
+ ('en', 'settings.tool_test.array_placeholder', 'Comma-separated values'),
+ ('en', 'settings.tool_test.description', 'Enter input values to test this tool'),
+ ('en', 'settings.tool_test.failed', 'Failed'),
+ ('en', 'settings.tool_test.form', 'Form'),
+ ('en', 'settings.tool_test.no_parameters', 'This tool has no input parameters'),
+ ('en', 'settings.tool_test.run_test', 'Run Test'),
+ ('en', 'settings.tool_test.select_function', 'Select Function'),
+ ('en', 'settings.tool_test.select_placeholder', 'Select a function...'),
+ ('en', 'settings.tool_test.success', 'Success'),
+ ('en', 'settings.tool_test.test', 'Test'),
+ ('en', 'settings.tool_test.view_output', 'View output'),
+ ('en', 'settings.tools.add', 'Add Tool'),
+ ('en', 'settings.tools.args_placeholder', 'Arguments (comma-separated)'),
+ ('en', 'settings.tools.command_placeholder', 'Command (e.g. npx)'),
+ ('en', 'settings.tools.configure', 'Configure'),
+ ('en', 'settings.tools.configured', 'Configured'),
+ ('en', 'settings.tools.create', 'Create Tool'),
+ ('en', 'settings.tools.create_first', 'Create your first tool'),
+ ('en', 'settings.tools.description', 'Description'),
+ ('en', 'settings.tools.description_placeholder', 'Fetches content from a URL'),
+ ('en', 'settings.tools.display_name', 'Display Name'),
+ ('en', 'settings.tools.display_name_placeholder', 'Fetch Website'),
+ ('en', 'settings.tools.edit', 'Edit Tool'),
+ ('en', 'settings.tools.entrypoint', 'Entrypoint'),
+ ('en', 'settings.tools.function_description_placeholder', 'What this function does'),
+ ('en', 'settings.tools.function_name_placeholder', 'search_web'),
+ ('en', 'settings.tools.functions', 'Functions'),
+ ('en', 'settings.tools.general', 'General'),
+ ('en', 'settings.tools.headers', 'Headers (JSON)'),
+ ('en', 'settings.tools.headers_placeholder', '{"Authorization": "Bearer {{settings.api_key}}"}'),
+ ('en', 'settings.tools.identifier', 'Identifier'),
+ ('en', 'settings.tools.identifier_placeholder', 'fetch_website'),
+ ('en', 'settings.tools.image_model_description', 'Select the global image model used by this tool.'),
+ ('en', 'settings.tools.image_model_placeholder', 'Select image model'),
+ ('en', 'settings.tools.image_model_title', 'Image model'),
+ ('en', 'settings.tools.imagegen.description', 'Generate and edit images using an admin-selected provider model'),
+ ('en', 'settings.tools.imagegen.display_name', 'Image Generation'),
+ ('en', 'settings.tools.imagegen.edit.description', 'Edit a previously generated image using a text prompt'),
+ ('en', 'settings.tools.imagegen.edit.image_id', 'The image ID of a previously generated image to edit. Preferred over an image URL.'),
+ ('en', 'settings.tools.imagegen.edit.image_url', 'URL of an external image to edit. Only use when no image ID is available.'),
+ ('en', 'settings.tools.imagegen.edit.prompt', 'The text prompt describing the desired edit'),
+ ('en', 'settings.tools.imagegen.generate.description', 'Generate an image from a text prompt'),
+ ('en', 'settings.tools.imagegen.generate.prompt', 'The text prompt describing the image to generate'),
+ ('en', 'settings.tools.imagegen.generate.quality', 'Image quality'),
+ ('en', 'settings.tools.imagegen.generate.size', 'Image size'),
+ ('en', 'settings.tools.imagegen.settings.description', 'Select an enabled image model'),
+ ('en', 'settings.tools.input_schema', 'Input Schema'),
+ ('en', 'settings.tools.name', 'Name'),
+ ('en', 'settings.tools.new', 'New'),
+ ('en', 'settings.tools.no_description', 'No description'),
+ ('en', 'settings.tools.no_tools', 'No tools yet'),
+ ('en', 'settings.tools.no_tools_description', 'Create custom tools to extend AI capabilities'),
+ ('en', 'settings.tools.optional', 'Optional'),
+ ('en', 'settings.tools.remove', 'Remove'),
+ ('en', 'settings.tools.select_placeholder', 'Select...'),
+ ('en', 'settings.tools.settings', 'Tool Settings'),
+ ('en', 'settings.tools.settings_description', 'Configure your personal settings for {name}'),
+ ('en', 'settings.tools.settings_schema', 'Settings Schema (optional)'),
+ ('en', 'settings.tools.soon', 'Soon'),
+ ('en', 'settings.tools.source', 'Source'),
+ ('en', 'settings.tools.sse', 'SSE (HTTP)'),
+ ('en', 'settings.tools.stdio', 'Stdio (Local Process)'),
+ ('en', 'settings.tools.system_prompt', 'System prompt injection'),
+ ('en', 'settings.tools.system_prompt_hint', 'Prepended to the system prompt whenever this tool is enabled for a request. Use it to guide how the model uses the tool.'),
+ ('en', 'settings.tools.system_prompt_placeholder', 'Extra system instructions added when this tool is active'),
+ ('en', 'settings.tools.template', 'Template'),
+ ('en', 'settings.tools.title', 'Tools'),
+ ('en', 'settings.tools.tool_name_placeholder', 'Tool name from server'),
+ ('en', 'settings.tools.url_placeholder', 'https://api.example.com/{{input.query}}'),
+ ('en', 'sidebar.ai_chat_app', 'AI Chat Application'),
+ ('en', 'sidebar.all', 'All'),
+ ('en', 'sidebar.all_chats', 'All chats'),
+ ('en', 'sidebar.budget.remaining', '{amount} left'),
+ ('en', 'sidebar.chats', 'Chats'),
+ ('en', 'sidebar.logout', 'Log out'),
+ ('en', 'sidebar.manage_mcp', 'Manage MCP servers'),
+ ('en', 'sidebar.manage_workspaces', 'Manage workspaces'),
+ ('en', 'sidebar.new_chat', 'New Chat'),
+ ('en', 'sidebar.workspace', 'Workspace'),
+ ('en', 'welcome.description', 'Select a chat from the sidebar or start a new conversation'),
+ ('en', 'welcome.title', 'Welcome to OxideChat'),
+ ('en', 'workspace.cancel', 'Cancel'),
+ ('en', 'workspace.cannot_delete_default', 'The default workspace cannot be deleted.'),
+ ('en', 'workspace.chat_count', '{count} chats'),
+ ('en', 'workspace.color', 'Accent color'),
+ ('en', 'workspace.color_none', 'No color'),
+ ('en', 'workspace.confirm', 'Confirm'),
+ ('en', 'workspace.create', 'Create workspace'),
+ ('en', 'workspace.create_failed', 'Could not create the workspace.'),
+ ('en', 'workspace.default_badge', 'Default'),
+ ('en', 'workspace.delete', 'Delete'),
+ ('en', 'workspace.delete_action_archive', 'Archive the chats'),
+ ('en', 'workspace.delete_action_delete', 'Delete the chats permanently'),
+ ('en', 'workspace.delete_action_move', 'Move chats to another workspace'),
+ ('en', 'workspace.delete_description', 'Choose what happens to the chats in "{name}".'),
+ ('en', 'workspace.delete_failed', 'Could not delete the workspace.'),
+ ('en', 'workspace.delete_move_target', 'Move to'),
+ ('en', 'workspace.delete_title', 'Delete workspace'),
+ ('en', 'workspace.empty', 'No workspaces yet.'),
+ ('en', 'workspace.manage_description', 'Create, rename, recolor, and delete your workspaces.'),
+ ('en', 'workspace.manage_title', 'Workspaces'),
+ ('en', 'workspace.name', 'Name'),
+ ('en', 'workspace.name_placeholder', 'Workspace name'),
+ ('en', 'workspace.rename', 'Rename'),
+ ('en', 'workspace.save', 'Save'),
+ ('en', 'workspace.set_default', 'Set as default'),
+ ('en', 'workspace.update_failed', 'Could not update the workspace.')
ON CONFLICT (language, key_path) DO NOTHING;
diff --git a/migrations/20260623000000_gateway_catalog.sql b/migrations/20260623000000_gateway_catalog.sql
deleted file mode 100644
index b946f09..0000000
--- a/migrations/20260623000000_gateway_catalog.sql
+++ /dev/null
@@ -1,106 +0,0 @@
--- Gateway catalog: persists a gateway's full model catalog separately from runnable `models`.
---
--- A gateway (V1: OpenRouter) exposes more models than the configured API key can actually
--- run. `gateway_catalog_models` stores every public catalog model — including those the key
--- cannot use (`USER_UNAVAILABLE`) — and optionally links to a runnable `models` row via
--- `local_model_id`. `gateway_model_provider_options` stores per-endpoint provider options for
--- a catalog model, fetched lazily.
-
-DO $$ BEGIN
- CREATE TYPE gateway_availability AS ENUM (
- 'AVAILABLE',
- 'USER_UNAVAILABLE'
- );
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
-
-CREATE TABLE IF NOT EXISTS gateway_catalog_models (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- provider_id UUID NOT NULL REFERENCES providers(id) ON DELETE CASCADE,
- source_gateway VARCHAR(50) NOT NULL, -- e.g. 'openrouter'
- gateway_model_id TEXT NOT NULL, -- bare 'author/slug'
- local_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
- display_name TEXT NOT NULL DEFAULT '',
- availability_state gateway_availability NOT NULL DEFAULT 'AVAILABLE',
- reason TEXT,
- raw JSONB NOT NULL DEFAULT '{}',
- fetched_at TIMESTAMPTZ,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- UNIQUE (provider_id, source_gateway, gateway_model_id)
-);
-
-CREATE TABLE IF NOT EXISTS gateway_model_provider_options (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- catalog_model_id UUID NOT NULL REFERENCES gateway_catalog_models(id) ON DELETE CASCADE,
- provider_slug TEXT, -- endpoint tag, e.g. 'openai'
- provider_name TEXT,
- endpoint_name TEXT,
- status DOUBLE PRECISION, -- OpenRouter health status (0 = healthy)
- quantization TEXT,
- context_length INTEGER,
- max_completion_tokens INTEGER,
- latency DOUBLE PRECISION, -- p50 latency (seconds)
- throughput DOUBLE PRECISION, -- p50 throughput (tokens/sec)
- uptime DOUBLE PRECISION, -- uptime over last 30m (percent)
- price_input DOUBLE PRECISION, -- USD per million input tokens
- price_output DOUBLE PRECISION, -- USD per million output tokens
- raw JSONB NOT NULL DEFAULT '{}',
- fetched_at TIMESTAMPTZ,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-);
-
-CREATE INDEX IF NOT EXISTS idx_gateway_catalog_models_provider ON gateway_catalog_models(provider_id);
-CREATE INDEX IF NOT EXISTS idx_gateway_catalog_models_local ON gateway_catalog_models(local_model_id);
-CREATE INDEX IF NOT EXISTS idx_gateway_catalog_models_availability ON gateway_catalog_models(availability_state);
-CREATE INDEX IF NOT EXISTS idx_gateway_provider_options_catalog ON gateway_model_provider_options(catalog_model_id);
-
--- UI translations for the catalog tab and the model provider-options table.
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'settings.providers.tab_settings', 'Settings'),
- ('en', 'settings.providers.tab_catalog', 'Catalog'),
- ('en', 'settings.providers.catalog_search', 'Search catalog models...'),
- ('en', 'settings.providers.catalog_empty', 'No catalog models found'),
- ('en', 'settings.providers.catalog_disabled_key', 'Disabled for this key'),
- ('en', 'settings.providers.catalog_available', 'Available'),
- ('en', 'settings.models.editor.providers', 'Providers'),
- ('en', 'settings.models.editor.providers_desc', 'Provider endpoints that can serve this model, with status and pricing.'),
- ('en', 'settings.models.editor.providers_refresh', 'Refresh'),
- ('en', 'settings.models.editor.providers_user_unavailable', 'This model is disabled for the configured API key.'),
- ('en', 'settings.models.editor.providers_empty', 'No provider options available'),
- ('en', 'settings.models.editor.providers_load_error', 'Failed to load provider options'),
- ('en', 'settings.models.editor.provider_col_provider', 'Provider'),
- ('en', 'settings.models.editor.provider_col_status', 'Status'),
- ('en', 'settings.models.editor.provider_col_quant', 'Quantization'),
- ('en', 'settings.models.editor.provider_col_context', 'Context'),
- ('en', 'settings.models.editor.provider_col_price', 'Price (in/out per 1M)'),
- ('en', 'settings.models.editor.provider_col_uptime', 'Uptime'),
- ('en', 'settings.models.editor.row_available', 'Available'),
- ('en', 'settings.models.editor.row_unavailable', 'Unavailable'),
- ('en', 'settings.models.editor.row_user_unavailable', 'Disabled for this key'),
- ('en', 'settings.models.editor.row_unknown', 'Unknown'),
- ('de', 'settings.providers.tab_settings', 'Einstellungen'),
- ('de', 'settings.providers.tab_catalog', 'Katalog'),
- ('de', 'settings.providers.catalog_search', 'Katalogmodelle suchen...'),
- ('de', 'settings.providers.catalog_empty', 'Keine Katalogmodelle gefunden'),
- ('de', 'settings.providers.catalog_disabled_key', 'Für diesen Schlüssel deaktiviert'),
- ('de', 'settings.providers.catalog_available', 'Verfügbar'),
- ('de', 'settings.models.editor.providers', 'Anbieter'),
- ('de', 'settings.models.editor.providers_desc', 'Anbieter-Endpunkte, die dieses Modell bereitstellen können, mit Status und Preisen.'),
- ('de', 'settings.models.editor.providers_refresh', 'Aktualisieren'),
- ('de', 'settings.models.editor.providers_user_unavailable', 'Dieses Modell ist für den konfigurierten API-Schlüssel deaktiviert.'),
- ('de', 'settings.models.editor.providers_empty', 'Keine Anbieteroptionen verfügbar'),
- ('de', 'settings.models.editor.providers_load_error', 'Anbieteroptionen konnten nicht geladen werden'),
- ('de', 'settings.models.editor.provider_col_provider', 'Anbieter'),
- ('de', 'settings.models.editor.provider_col_status', 'Status'),
- ('de', 'settings.models.editor.provider_col_quant', 'Quantisierung'),
- ('de', 'settings.models.editor.provider_col_context', 'Kontext'),
- ('de', 'settings.models.editor.provider_col_price', 'Preis (Ein/Aus pro 1M)'),
- ('de', 'settings.models.editor.provider_col_uptime', 'Verfügbarkeit'),
- ('de', 'settings.models.editor.row_available', 'Verfügbar'),
- ('de', 'settings.models.editor.row_unavailable', 'Nicht verfügbar'),
- ('de', 'settings.models.editor.row_user_unavailable', 'Für diesen Schlüssel deaktiviert'),
- ('de', 'settings.models.editor.row_unknown', 'Unbekannt')
-ON CONFLICT (language, key_path) DO NOTHING;
diff --git a/migrations/20260624000000_provider_selector_i18n.sql b/migrations/20260624000000_provider_selector_i18n.sql
deleted file mode 100644
index 66dbc6a..0000000
--- a/migrations/20260624000000_provider_selector_i18n.sql
+++ /dev/null
@@ -1,33 +0,0 @@
--- UI translations for the chat upstream-provider selector and its admin toggle.
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'chat.provider_selector.title', 'Provider'),
- ('en', 'chat.provider_selector.mode_prefer', 'Prefer'),
- ('en', 'chat.provider_selector.mode_lock', 'Lock'),
- ('en', 'chat.provider_selector.automatic', 'Automatic'),
- ('en', 'chat.provider_selector.automatic_hint', 'Let the gateway choose the provider'),
- ('en', 'chat.provider_selector.unavailable_for_key', 'This model is disabled for the configured key'),
- ('en', 'chat.provider_selector.no_providers', 'No provider options available'),
- ('en', 'chat.provider_selector.col_provider', 'Provider'),
- ('en', 'chat.provider_selector.col_price', 'In / Out /M'),
- ('en', 'chat.provider_selector.col_latency', 'Latency'),
- ('en', 'chat.provider_selector.col_throughput', 'Throughput'),
- ('en', 'settings.admin_config.provider_selector.label', 'Chat provider selector'),
- ('en', 'settings.admin_config.provider_selector.hint', 'Let users pick which upstream provider serves each OpenRouter request.'),
- ('en', 'settings.models.editor.provider_col_latency', 'Latency'),
- ('en', 'settings.models.editor.provider_col_throughput', 'Throughput'),
- ('de', 'chat.provider_selector.title', 'Anbieter'),
- ('de', 'chat.provider_selector.mode_prefer', 'Bevorzugen'),
- ('de', 'chat.provider_selector.mode_lock', 'Sperren'),
- ('de', 'chat.provider_selector.automatic', 'Automatisch'),
- ('de', 'chat.provider_selector.automatic_hint', 'Das Gateway den Anbieter wählen lassen'),
- ('de', 'chat.provider_selector.unavailable_for_key', 'Dieses Modell ist für den konfigurierten Schlüssel deaktiviert'),
- ('de', 'chat.provider_selector.no_providers', 'Keine Anbieteroptionen verfügbar'),
- ('de', 'chat.provider_selector.col_provider', 'Anbieter'),
- ('de', 'chat.provider_selector.col_price', 'Ein / Aus /M'),
- ('de', 'chat.provider_selector.col_latency', 'Latenz'),
- ('de', 'chat.provider_selector.col_throughput', 'Durchsatz'),
- ('de', 'settings.admin_config.provider_selector.label', 'Chat-Anbieterauswahl'),
- ('de', 'settings.admin_config.provider_selector.hint', 'Benutzer wählen lassen, welcher Upstream-Anbieter jede OpenRouter-Anfrage bedient.'),
- ('de', 'settings.models.editor.provider_col_latency', 'Latenz'),
- ('de', 'settings.models.editor.provider_col_throughput', 'Durchsatz')
-ON CONFLICT (language, key_path) DO NOTHING;
diff --git a/migrations/20260625000000_provider_selector_i18n_v2.sql b/migrations/20260625000000_provider_selector_i18n_v2.sql
deleted file mode 100644
index de30229..0000000
--- a/migrations/20260625000000_provider_selector_i18n_v2.sql
+++ /dev/null
@@ -1,26 +0,0 @@
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'chat.provider_selector.search_placeholder', 'Search providers...'),
- ('en', 'chat.provider_selector.sort_label', 'Sort'),
- ('en', 'chat.provider_selector.sort_recommended', 'Recommended'),
- ('en', 'chat.provider_selector.sort_latency', 'Latency'),
- ('en', 'chat.provider_selector.sort_price', 'Price'),
- ('en', 'chat.provider_selector.sort_throughput', 'Throughput'),
- ('en', 'chat.provider_selector.col_context', 'Context'),
- ('en', 'chat.provider_selector.expanded_privacy', 'Privacy'),
- ('en', 'chat.provider_selector.expanded_uptime', 'Uptime'),
- ('en', 'chat.provider_selector.expanded_rate_limits', 'Rate limits'),
- ('en', 'chat.provider_selector.expanded_tools', 'Tools'),
- ('en', 'chat.provider_selector.privacy_on', 'On'),
- ('de', 'chat.provider_selector.search_placeholder', 'Anbieter suchen...'),
- ('de', 'chat.provider_selector.sort_label', 'Sortierung'),
- ('de', 'chat.provider_selector.sort_recommended', 'Empfohlen'),
- ('de', 'chat.provider_selector.sort_latency', 'Latenz'),
- ('de', 'chat.provider_selector.sort_price', 'Preis'),
- ('de', 'chat.provider_selector.sort_throughput', 'Durchsatz'),
- ('de', 'chat.provider_selector.col_context', 'Kontext'),
- ('de', 'chat.provider_selector.expanded_privacy', 'Datenschutz'),
- ('de', 'chat.provider_selector.expanded_uptime', 'Verfügbarkeit'),
- ('de', 'chat.provider_selector.expanded_rate_limits', 'Rate-Limits'),
- ('de', 'chat.provider_selector.expanded_tools', 'Tools'),
- ('de', 'chat.provider_selector.privacy_on', 'An')
-ON CONFLICT (language, key_path) DO NOTHING;
diff --git a/migrations/20260629000000_provider_selector_detail_i18n.sql b/migrations/20260629000000_provider_selector_detail_i18n.sql
deleted file mode 100644
index 0b18939..0000000
--- a/migrations/20260629000000_provider_selector_detail_i18n.sql
+++ /dev/null
@@ -1,28 +0,0 @@
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'chat.provider_selector.col_uptime', 'Uptime'),
- ('en', 'chat.provider_selector.price_io_label', 'Input / Output'),
- ('en', 'chat.provider_selector.latency_estimated', 'Estimated'),
- ('en', 'chat.provider_selector.context_tokens', 'Tokens'),
- ('en', 'chat.provider_selector.status_healthy', 'Healthy'),
- ('en', 'chat.provider_selector.status_degraded', 'Degraded'),
- ('en', 'chat.provider_selector.status_all_operational', 'All systems operational'),
- ('en', 'chat.provider_selector.status_some_issues', 'Some issues detected'),
- ('en', 'chat.provider_selector.sort_recommended', 'Recommended'),
- ('en', 'chat.provider_selector.sort_latency', 'Latency'),
- ('en', 'chat.provider_selector.sort_throughput', 'Throughput'),
- ('en', 'chat.provider_selector.sort_price', 'Price'),
- ('en', 'chat.provider_selector.sort_uptime', 'Uptime'),
- ('de', 'chat.provider_selector.col_uptime', 'Verfügbarkeit'),
- ('de', 'chat.provider_selector.price_io_label', 'Ein / Aus'),
- ('de', 'chat.provider_selector.latency_estimated', 'Geschätzt'),
- ('de', 'chat.provider_selector.context_tokens', 'Token'),
- ('de', 'chat.provider_selector.status_healthy', 'Gesund'),
- ('de', 'chat.provider_selector.status_degraded', 'Beeinträchtigt'),
- ('de', 'chat.provider_selector.status_all_operational', 'Alle Systeme betriebsbereit'),
- ('de', 'chat.provider_selector.status_some_issues', 'Einige Probleme erkannt'),
- ('de', 'chat.provider_selector.sort_recommended', 'Empfohlen'),
- ('de', 'chat.provider_selector.sort_latency', 'Latenz'),
- ('de', 'chat.provider_selector.sort_throughput', 'Durchsatz'),
- ('de', 'chat.provider_selector.sort_price', 'Preis'),
- ('de', 'chat.provider_selector.sort_uptime', 'Verfügbarkeit')
-ON CONFLICT (language, key_path) DO NOTHING;
diff --git a/migrations/20260701000000_workspace_management_i18n.sql b/migrations/20260701000000_workspace_management_i18n.sql
deleted file mode 100644
index cb75678..0000000
--- a/migrations/20260701000000_workspace_management_i18n.sql
+++ /dev/null
@@ -1,59 +0,0 @@
--- UI translations for workspace management (create / rename / delete dialogs).
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'sidebar.manage_workspaces', 'Manage workspaces'),
- ('en', 'workspace.manage_title', 'Workspaces'),
- ('en', 'workspace.manage_description', 'Create, rename, recolor, and delete your workspaces.'),
- ('en', 'workspace.create', 'Create workspace'),
- ('en', 'workspace.rename', 'Rename'),
- ('en', 'workspace.delete', 'Delete'),
- ('en', 'workspace.save', 'Save'),
- ('en', 'workspace.name', 'Name'),
- ('en', 'workspace.name_placeholder', 'Workspace name'),
- ('en', 'workspace.color', 'Accent color'),
- ('en', 'workspace.color_none', 'No color'),
- ('en', 'workspace.set_default', 'Set as default'),
- ('en', 'workspace.default_badge', 'Default'),
- ('en', 'workspace.chat_count', '{count} chats'),
- ('en', 'workspace.empty', 'No workspaces yet.'),
- ('en', 'workspace.cannot_delete_default', 'The default workspace cannot be deleted.'),
- ('en', 'workspace.delete_title', 'Delete workspace'),
- ('en', 'workspace.delete_description', 'Choose what happens to the chats in "{name}".'),
- ('en', 'workspace.delete_action_move', 'Move chats to another workspace'),
- ('en', 'workspace.delete_action_archive', 'Archive the chats'),
- ('en', 'workspace.delete_action_delete', 'Delete the chats permanently'),
- ('en', 'workspace.delete_move_target', 'Move to'),
- ('en', 'workspace.confirm', 'Confirm'),
- ('en', 'workspace.cancel', 'Cancel'),
- ('en', 'workspace.create_failed', 'Could not create the workspace.'),
- ('en', 'workspace.update_failed', 'Could not update the workspace.'),
- ('en', 'workspace.delete_failed', 'Could not delete the workspace.'),
- ('en', 'chat.context_menu.no_workspace', 'No workspace'),
- ('de', 'sidebar.manage_workspaces', 'Arbeitsbereiche verwalten'),
- ('de', 'workspace.manage_title', 'Arbeitsbereiche'),
- ('de', 'workspace.manage_description', 'Arbeitsbereiche erstellen, umbenennen, einfärben und löschen.'),
- ('de', 'workspace.create', 'Arbeitsbereich erstellen'),
- ('de', 'workspace.rename', 'Umbenennen'),
- ('de', 'workspace.delete', 'Löschen'),
- ('de', 'workspace.save', 'Speichern'),
- ('de', 'workspace.name', 'Name'),
- ('de', 'workspace.name_placeholder', 'Name des Arbeitsbereichs'),
- ('de', 'workspace.color', 'Akzentfarbe'),
- ('de', 'workspace.color_none', 'Keine Farbe'),
- ('de', 'workspace.set_default', 'Als Standard festlegen'),
- ('de', 'workspace.default_badge', 'Standard'),
- ('de', 'workspace.chat_count', '{count} Chats'),
- ('de', 'workspace.empty', 'Noch keine Arbeitsbereiche.'),
- ('de', 'workspace.cannot_delete_default', 'Der Standard-Arbeitsbereich kann nicht gelöscht werden.'),
- ('de', 'workspace.delete_title', 'Arbeitsbereich löschen'),
- ('de', 'workspace.delete_description', 'Wähle, was mit den Chats in "{name}" passiert.'),
- ('de', 'workspace.delete_action_move', 'Chats in einen anderen Arbeitsbereich verschieben'),
- ('de', 'workspace.delete_action_archive', 'Chats archivieren'),
- ('de', 'workspace.delete_action_delete', 'Chats endgültig löschen'),
- ('de', 'workspace.delete_move_target', 'Verschieben nach'),
- ('de', 'workspace.confirm', 'Bestätigen'),
- ('de', 'workspace.cancel', 'Abbrechen'),
- ('de', 'workspace.create_failed', 'Arbeitsbereich konnte nicht erstellt werden.'),
- ('de', 'workspace.update_failed', 'Arbeitsbereich konnte nicht aktualisiert werden.'),
- ('de', 'workspace.delete_failed', 'Arbeitsbereich konnte nicht gelöscht werden.'),
- ('de', 'chat.context_menu.no_workspace', 'Kein Arbeitsbereich')
-ON CONFLICT (language, key_path) DO NOTHING;
diff --git a/migrations/20260703000000_teams.sql b/migrations/20260703000000_teams.sql
deleted file mode 100644
index 0497fc7..0000000
--- a/migrations/20260703000000_teams.sql
+++ /dev/null
@@ -1,148 +0,0 @@
-CREATE TABLE IF NOT EXISTS teams (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- name VARCHAR(100) NOT NULL UNIQUE,
- description TEXT,
- is_default BOOLEAN NOT NULL DEFAULT false,
- allow_all_models BOOLEAN NOT NULL DEFAULT false,
- budget_id UUID,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-);
-
-CREATE UNIQUE INDEX IF NOT EXISTS teams_single_default ON teams (is_default) WHERE is_default = true;
-
-CREATE TABLE IF NOT EXISTS team_members (
- team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- PRIMARY KEY (team_id, user_id)
-);
-
-CREATE TABLE IF NOT EXISTS team_model_access (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
- provider_id UUID REFERENCES providers(id) ON DELETE CASCADE,
- model_id UUID REFERENCES models(id) ON DELETE CASCADE,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- CONSTRAINT team_model_access_target_check CHECK (
- (provider_id IS NOT NULL AND model_id IS NULL) OR
- (provider_id IS NULL AND model_id IS NOT NULL)
- )
-);
-
-CREATE UNIQUE INDEX IF NOT EXISTS team_model_access_provider_unique ON team_model_access (team_id, provider_id) WHERE provider_id IS NOT NULL;
-CREATE UNIQUE INDEX IF NOT EXISTS team_model_access_model_unique ON team_model_access (team_id, model_id) WHERE model_id IS NOT NULL;
-
-CREATE TABLE IF NOT EXISTS team_permissions (
- team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
- permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
- is_allowed BOOLEAN NOT NULL DEFAULT true,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- PRIMARY KEY (team_id, permission_id)
-);
-
-CREATE INDEX IF NOT EXISTS idx_teams_default ON teams(is_default) WHERE is_default = true;
-CREATE INDEX IF NOT EXISTS idx_team_members_team ON team_members(team_id);
-CREATE INDEX IF NOT EXISTS idx_team_members_user ON team_members(user_id);
-CREATE INDEX IF NOT EXISTS idx_team_model_access_team ON team_model_access(team_id);
-CREATE INDEX IF NOT EXISTS idx_team_model_access_provider ON team_model_access(provider_id);
-CREATE INDEX IF NOT EXISTS idx_team_model_access_model ON team_model_access(model_id);
-CREATE INDEX IF NOT EXISTS idx_team_permissions_permission ON team_permissions(permission_id);
-
-INSERT INTO permissions (name, description) VALUES
- ('admin.teams.view', 'View teams'),
- ('admin.teams.edit', 'Manage teams')
-ON CONFLICT (name) DO NOTHING;
-
-INSERT INTO teams (name, description, is_default, allow_all_models)
-VALUES ('General', 'Default team for all users', true, true)
-ON CONFLICT (name) DO UPDATE
- SET is_default = true,
- allow_all_models = true,
- updated_at = NOW();
-
-INSERT INTO team_members (team_id, user_id)
-SELECT t.id, u.id
-FROM teams t
-CROSS JOIN users u
-WHERE t.name = 'General'
-ON CONFLICT DO NOTHING;
-
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'settings.tabs.teams', 'Teams'),
- ('en', 'settings.teams.description', 'Organize users, model access, and budgets'),
- ('en', 'settings.teams.create', 'Create Team'),
- ('en', 'settings.teams.search', 'Search teams...'),
- ('en', 'settings.teams.default', 'Default'),
- ('en', 'settings.teams.members_count', '{count} members'),
- ('en', 'settings.teams.name', 'Name'),
- ('en', 'settings.teams.description_field', 'Description'),
- ('en', 'settings.teams.allow_all_models', 'Allow all models'),
- ('en', 'settings.teams.allow_all_models_hint', 'Members can use every enabled model. Turn off to restrict access.'),
- ('en', 'settings.teams.members', 'Members'),
- ('en', 'settings.teams.models', 'Models'),
- ('en', 'settings.teams.search_members', 'Search members...'),
- ('en', 'settings.teams.search_models', 'Search models...'),
- ('en', 'settings.teams.providers', 'Providers'),
- ('en', 'settings.teams.budget', 'Budget'),
- ('en', 'settings.teams.no_members', 'No users found'),
- ('en', 'settings.teams.no_models', 'No models found'),
- ('en', 'settings.teams.selected_count', '{count} selected'),
- ('en', 'settings.teams.no_selection_title', 'No team selected'),
- ('en', 'settings.teams.no_selection', 'Select a team to manage its members, models, and budget.'),
- ('en', 'settings.teams.confirm_delete', 'Delete this team? Members keep access through their other teams.'),
- ('en', 'settings.teams.load_error', 'Failed to load teams'),
- ('en', 'settings.teams.create_success', 'Team created'),
- ('en', 'settings.teams.create_error', 'Failed to create team'),
- ('en', 'settings.teams.save_success', 'Team saved'),
- ('en', 'settings.teams.save_error', 'Failed to save team'),
- ('en', 'settings.teams.delete_success', 'Team deleted'),
- ('en', 'settings.teams.delete_error', 'Failed to delete team'),
- ('en', 'settings.teams.budget_link_title', 'Budget configuration'),
- ('en', 'settings.teams.budget_link_hint', 'Link this team to a budget configuration to track and enforce usage limits.'),
- ('en', 'settings.teams.budget_not_linked', 'No budget linked'),
- ('en', 'settings.teams.budget_linked', 'Linked budget'),
- ('en', 'settings.teams.budget_none_available', 'No budget configurations exist yet. Once created, you can link one here.'),
- ('en', 'settings.teams.budget_unlink', 'Unlink'),
- ('en', 'settings.admin_users.field_teams', 'Teams'),
- ('en', 'settings.admin_users.select_teams', 'Select teams'),
- ('en', 'settings.admin_users.filter_team', 'Filter team'),
- ('de', 'settings.tabs.teams', 'Teams'),
- ('de', 'settings.teams.description', 'Benutzer, Modellzugriff und Budgets organisieren'),
- ('de', 'settings.teams.create', 'Team erstellen'),
- ('de', 'settings.teams.search', 'Teams suchen...'),
- ('de', 'settings.teams.default', 'Standard'),
- ('de', 'settings.teams.members_count', '{count} Mitglieder'),
- ('de', 'settings.teams.name', 'Name'),
- ('de', 'settings.teams.description_field', 'Beschreibung'),
- ('de', 'settings.teams.allow_all_models', 'Alle Modelle erlauben'),
- ('de', 'settings.teams.allow_all_models_hint', 'Mitglieder können jedes aktivierte Modell nutzen. Deaktivieren, um den Zugriff einzuschränken.'),
- ('de', 'settings.teams.members', 'Mitglieder'),
- ('de', 'settings.teams.models', 'Modelle'),
- ('de', 'settings.teams.search_members', 'Mitglieder suchen...'),
- ('de', 'settings.teams.search_models', 'Modelle suchen...'),
- ('de', 'settings.teams.providers', 'Anbieter'),
- ('de', 'settings.teams.budget', 'Budget'),
- ('de', 'settings.teams.no_members', 'Keine Benutzer gefunden'),
- ('de', 'settings.teams.no_models', 'Keine Modelle gefunden'),
- ('de', 'settings.teams.selected_count', '{count} ausgewählt'),
- ('de', 'settings.teams.no_selection_title', 'Kein Team ausgewählt'),
- ('de', 'settings.teams.no_selection', 'Wähle ein Team, um Mitglieder, Modelle und Budget zu verwalten.'),
- ('de', 'settings.teams.confirm_delete', 'Dieses Team löschen? Mitglieder behalten den Zugriff über ihre anderen Teams.'),
- ('de', 'settings.teams.load_error', 'Teams konnten nicht geladen werden'),
- ('de', 'settings.teams.create_success', 'Team erstellt'),
- ('de', 'settings.teams.create_error', 'Team konnte nicht erstellt werden'),
- ('de', 'settings.teams.save_success', 'Team gespeichert'),
- ('de', 'settings.teams.save_error', 'Team konnte nicht gespeichert werden'),
- ('de', 'settings.teams.delete_success', 'Team gelöscht'),
- ('de', 'settings.teams.delete_error', 'Team konnte nicht gelöscht werden'),
- ('de', 'settings.teams.budget_link_title', 'Budget-Konfiguration'),
- ('de', 'settings.teams.budget_link_hint', 'Verknüpfe dieses Team mit einer Budget-Konfiguration, um Nutzungslimits zu verfolgen und durchzusetzen.'),
- ('de', 'settings.teams.budget_not_linked', 'Kein Budget verknüpft'),
- ('de', 'settings.teams.budget_linked', 'Verknüpftes Budget'),
- ('de', 'settings.teams.budget_none_available', 'Es existieren noch keine Budget-Konfigurationen. Sobald eine erstellt wurde, kannst du sie hier verknüpfen.'),
- ('de', 'settings.teams.budget_unlink', 'Trennen'),
- ('de', 'settings.admin_users.field_teams', 'Teams'),
- ('de', 'settings.admin_users.select_teams', 'Teams auswählen'),
- ('de', 'settings.admin_users.filter_team', 'Nach Team filtern')
-ON CONFLICT (language, key_path) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW();
diff --git a/migrations/20260704000000_chat_tools_and_request_settings.sql b/migrations/20260704000000_chat_tools_and_request_settings.sql
deleted file mode 100644
index f045980..0000000
--- a/migrations/20260704000000_chat_tools_and_request_settings.sql
+++ /dev/null
@@ -1,114 +0,0 @@
--- Pending chat tooling and composer-state migration.
-
--- MCP (Model Context Protocol) support: instance config flag + UI translations.
-INSERT INTO app_config (key, value)
-VALUES ('allow_server_stdio_mcp', 'false')
-ON CONFLICT (key) DO NOTHING;
-
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'sidebar.manage_mcp', 'Manage MCP servers'),
- ('en', 'mcp.manage_title', 'MCP servers'),
- ('en', 'mcp.manage_description', 'Connect remote Model Context Protocol servers to add tools to your chats.'),
- ('en', 'mcp.tool_selector.manage', 'Manage MCP servers…'),
- ('en', 'mcp.add', 'Add server'),
- ('en', 'mcp.edit', 'Edit'),
- ('en', 'mcp.delete', 'Delete'),
- ('en', 'mcp.save', 'Save'),
- ('en', 'mcp.cancel', 'Cancel'),
- ('en', 'mcp.name', 'Name'),
- ('en', 'mcp.name_placeholder', 'My MCP server'),
- ('en', 'mcp.transport', 'Transport'),
- ('en', 'mcp.transport_http', 'Streamable HTTP'),
- ('en', 'mcp.transport_stdio', 'stdio (server-side)'),
- ('en', 'mcp.url', 'Server URL'),
- ('en', 'mcp.url_placeholder', 'https://example.com/mcp'),
- ('en', 'mcp.headers', 'Headers (JSON)'),
- ('en', 'mcp.command', 'Command'),
- ('en', 'mcp.args', 'Arguments (one per line)'),
- ('en', 'mcp.enabled', 'Enabled'),
- ('en', 'mcp.discover', 'Test & discover tools'),
- ('en', 'mcp.discovered_tools', 'Discovered tools'),
- ('en', 'mcp.no_servers', 'No MCP servers yet.'),
- ('en', 'mcp.health', 'Health'),
- ('en', 'mcp.health_healthy', 'Healthy'),
- ('en', 'mcp.health_unhealthy', 'Unhealthy'),
- ('en', 'mcp.health_unknown', 'Unknown'),
- ('en', 'mcp.create_failed', 'Could not create the MCP server.'),
- ('en', 'mcp.update_failed', 'Could not update the MCP server.'),
- ('en', 'mcp.delete_failed', 'Could not delete the MCP server.'),
- ('en', 'mcp.discover_failed', 'Could not connect to the MCP server.'),
- ('en', 'admin.tools.mcp.allow_stdio', 'Allow server-side stdio MCP servers'),
- ('en', 'admin.tools.mcp.allow_stdio_hint', 'When enabled, global stdio MCP servers run arbitrary commands on the OxideChat host. Only enable on trusted, single-tenant instances.'),
- ('en', 'mcp.headers_name', 'Header name'),
- ('en', 'mcp.headers_value', 'Header value'),
- ('en', 'mcp.headers_add', 'Add header'),
- ('en', 'mcp.headers_empty', 'No headers added yet.'),
- ('en', 'mcp.headers_remove', 'Remove header'),
- ('en', 'chat.tool_selector.title', 'Tools'),
- ('en', 'chat.tool_selector.tools', 'tools'),
- ('en', 'chat.tool_selector.enabled', 'Enabled'),
- ('en', 'chat.tool_selector.available', 'Available'),
- ('en', 'chat.tool_selector.select_all', 'Select all'),
- ('en', 'chat.tool_selector.clear', 'Clear'),
- ('en', 'chat.tool_selector.no_tools', 'No tools available'),
- ('en', 'chat.tool_selector.no_tools_hint', 'Add an MCP server or a tool to get started.'),
- ('en', 'chat.tool_selector.count', '{count} selected'),
- ('en', 'chat.tool_selector.expand', 'Expand'),
- ('en', 'chat.tool_selector.collapse', 'Collapse'),
- ('en', 'chat.tool_selector.sub_tools', '{count} sub-tools'),
- ('en', 'chat.tool_selector.unknown_server', 'Unknown MCP server'),
- ('de', 'sidebar.manage_mcp', 'MCP-Server verwalten'),
- ('de', 'mcp.manage_title', 'MCP-Server'),
- ('de', 'mcp.manage_description', 'Verbinde entfernte Model-Context-Protocol-Server, um deinen Chats Werkzeuge hinzuzufügen.'),
- ('de', 'mcp.tool_selector.manage', 'MCP-Server verwalten…'),
- ('de', 'mcp.add', 'Server hinzufügen'),
- ('de', 'mcp.edit', 'Bearbeiten'),
- ('de', 'mcp.delete', 'Löschen'),
- ('de', 'mcp.save', 'Speichern'),
- ('de', 'mcp.cancel', 'Abbrechen'),
- ('de', 'mcp.name', 'Name'),
- ('de', 'mcp.name_placeholder', 'Mein MCP-Server'),
- ('de', 'mcp.transport', 'Transport'),
- ('de', 'mcp.transport_http', 'Streamable HTTP'),
- ('de', 'mcp.transport_stdio', 'stdio (serverseitig)'),
- ('de', 'mcp.url', 'Server-URL'),
- ('de', 'mcp.url_placeholder', 'https://example.com/mcp'),
- ('de', 'mcp.headers', 'Header (JSON)'),
- ('de', 'mcp.command', 'Befehl'),
- ('de', 'mcp.args', 'Argumente (eines pro Zeile)'),
- ('de', 'mcp.enabled', 'Aktiviert'),
- ('de', 'mcp.discover', 'Werkzeuge testen & finden'),
- ('de', 'mcp.discovered_tools', 'Gefundene Werkzeuge'),
- ('de', 'mcp.no_servers', 'Noch keine MCP-Server.'),
- ('de', 'mcp.health', 'Status'),
- ('de', 'mcp.health_healthy', 'Erreichbar'),
- ('de', 'mcp.health_unhealthy', 'Nicht erreichbar'),
- ('de', 'mcp.health_unknown', 'Unbekannt'),
- ('de', 'mcp.create_failed', 'Der MCP-Server konnte nicht erstellt werden.'),
- ('de', 'mcp.update_failed', 'Der MCP-Server konnte nicht aktualisiert werden.'),
- ('de', 'mcp.delete_failed', 'Der MCP-Server konnte nicht gelöscht werden.'),
- ('de', 'mcp.discover_failed', 'Verbindung zum MCP-Server fehlgeschlagen.'),
- ('de', 'admin.tools.mcp.allow_stdio', 'Serverseitige stdio-MCP-Server erlauben'),
- ('de', 'admin.tools.mcp.allow_stdio_hint', 'Wenn aktiviert, führen globale stdio-MCP-Server beliebige Befehle auf dem OxideChat-Host aus. Nur auf vertrauenswürdigen Einzelinstanzen aktivieren.'),
- ('de', 'mcp.headers_name', 'Header-Name'),
- ('de', 'mcp.headers_value', 'Header-Wert'),
- ('de', 'mcp.headers_add', 'Header hinzufügen'),
- ('de', 'mcp.headers_empty', 'Noch keine Header hinzugefügt.'),
- ('de', 'mcp.headers_remove', 'Header entfernen'),
- ('de', 'chat.tool_selector.title', 'Werkzeuge'),
- ('de', 'chat.tool_selector.tools', 'Werkzeuge'),
- ('de', 'chat.tool_selector.enabled', 'Aktiviert'),
- ('de', 'chat.tool_selector.available', 'Verfügbar'),
- ('de', 'chat.tool_selector.select_all', 'Alle auswählen'),
- ('de', 'chat.tool_selector.clear', 'Leeren'),
- ('de', 'chat.tool_selector.no_tools', 'Keine Werkzeuge verfügbar'),
- ('de', 'chat.tool_selector.no_tools_hint', 'Füge einen MCP-Server oder ein Werkzeug hinzu, um zu starten.'),
- ('de', 'chat.tool_selector.count', '{count} ausgewählt'),
- ('de', 'chat.tool_selector.expand', 'Ausklappen'),
- ('de', 'chat.tool_selector.collapse', 'Einklappen'),
- ('de', 'chat.tool_selector.sub_tools', '{count} Unterwerkzeuge'),
- ('de', 'chat.tool_selector.unknown_server', 'Unbekannter MCP-Server')
-ON CONFLICT (language, key_path) DO UPDATE SET value = EXCLUDED.value;
-
-ALTER TABLE messages
-ADD COLUMN IF NOT EXISTS request_settings JSONB NOT NULL DEFAULT '{}';
diff --git a/migrations/20260705000000_default_models.sql b/migrations/20260705000000_default_models.sql
deleted file mode 100644
index 41fa4da..0000000
--- a/migrations/20260705000000_default_models.sql
+++ /dev/null
@@ -1,49 +0,0 @@
-ALTER TABLE teams ADD COLUMN IF NOT EXISTS default_model_key VARCHAR(255);
-
-ALTER TABLE user_preferences ADD COLUMN IF NOT EXISTS default_provider_slug VARCHAR(255);
-ALTER TABLE user_preferences ADD COLUMN IF NOT EXISTS default_tools JSONB DEFAULT '[]';
-
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'settings.admin.default_model', 'Default model'),
- ('en', 'settings.admin.default_model_hint', 'Pre-select this model for all users when no team or personal default is set.'),
- ('en', 'settings.teams.default_model', 'Default model'),
- ('en', 'settings.teams.default_model_hint', 'Pre-select this model for team members. Overrides the global default.'),
- ('en', 'settings.teams.use_global_default', 'Use global default'),
- ('en', 'settings.profile.default_model', 'Default model'),
- ('en', 'settings.profile.default_model_hint', 'Pre-select this model when starting a new chat. Overrides team and global defaults.'),
- ('en', 'settings.profile.use_higher_default', 'Use team / global default'),
- ('de', 'settings.admin.default_model', 'Standardmodell'),
- ('de', 'settings.admin.default_model_hint', 'Dieses Modell für alle Benutzer vorauswählen, wenn kein Team- oder persönliches Standardmodell festgelegt ist.'),
- ('de', 'settings.teams.default_model', 'Standardmodell'),
- ('de', 'settings.teams.default_model_hint', 'Dieses Modell für Teammitglieder vorauswählen. Überschreibt das globale Standardmodell.'),
- ('de', 'settings.teams.use_global_default', 'Globalen Standard verwenden'),
- ('de', 'settings.profile.default_model', 'Standardmodell'),
- ('de', 'settings.profile.default_model_hint', 'Dieses Modell beim Starten eines neuen Chats vorauswählen. Überschreibt Team- und globale Standards.'),
- ('de', 'settings.profile.use_higher_default', 'Team- / globalen Standard verwenden'),
- ('en', 'chat.model_selector.toggle_default', 'Set as default model'),
- ('de', 'chat.model_selector.toggle_default', 'Als Standardmodell festlegen'),
- ('en', 'settings.profile.chat_defaults', 'Chat Defaults'),
- ('en', 'settings.profile.chat_defaults_description', 'Pre-configure model, provider, and tools for every new chat.'),
- ('en', 'settings.profile.default_provider', 'Default Provider'),
- ('en', 'settings.profile.default_provider_hint', 'Pre-select an upstream provider for the default model. Only visible for gateway models.'),
- ('en', 'settings.profile.default_provider_placeholder', 'Automatic'),
- ('en', 'settings.profile.default_tools', 'Default Tools'),
- ('en', 'settings.profile.default_tools_hint', 'These tools will be pre-enabled when starting a new chat.'),
- ('en', 'settings.profile.no_tools_available', 'No tools available. Connect an MCP server or add tools in the Tools settings.'),
- ('de', 'settings.profile.chat_defaults', 'Chat-Standardeinstellungen'),
- ('de', 'settings.profile.chat_defaults_description', 'Standardmäßiges Modell, Anbieter und Tools für jeden neuen Chat vorkonfigurieren.'),
- ('de', 'settings.profile.default_provider', 'Standard-Anbieter'),
- ('de', 'settings.profile.default_provider_hint', 'Einen vorgelagerten Anbieter für das Standardmodell vorauswählen. Nur für Gateway-Modelle sichtbar.'),
- ('de', 'settings.profile.default_provider_placeholder', 'Automatisch'),
- ('de', 'settings.profile.default_tools', 'Standard-Tools'),
- ('de', 'settings.profile.default_tools_hint', 'Diese Tools werden beim Starten eines neuen Chats vorausgewählt.'),
- ('de', 'settings.profile.no_tools_available', 'Keine Tools verfügbar. Verbinde einen MCP-Server oder füge Tools in den Tool-Einstellungen hinzu.'),
- ('en', 'settings.profile.save_success', 'Chat defaults saved.'),
- ('en', 'settings.profile.save_error', 'Failed to save chat defaults.'),
- ('de', 'settings.profile.save_success', 'Chat-Standardeinstellungen gespeichert.'),
- ('de', 'settings.profile.save_error', 'Chat-Standardeinstellungen konnten nicht gespeichert werden.'),
- ('en', 'chat.tool_selector.save_as_defaults', 'Save as defaults'),
- ('de', 'chat.tool_selector.save_as_defaults', 'Als Standard speichern'),
- ('en', 'chat.provider_selector.set_as_default', 'Set as default provider'),
- ('de', 'chat.provider_selector.set_as_default', 'Als Standard-Anbieter festlegen')
-ON CONFLICT (language, key_path) DO NOTHING;
diff --git a/migrations/20260706000000_budgets_and_analytics.sql b/migrations/20260706000000_budgets_and_analytics.sql
deleted file mode 100644
index e318ae8..0000000
--- a/migrations/20260706000000_budgets_and_analytics.sql
+++ /dev/null
@@ -1,330 +0,0 @@
-DO $$ BEGIN
- CREATE TYPE budget_kind AS ENUM ('pooled', 'per_user');
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
-
-DO $$ BEGIN
- CREATE TYPE budget_interval AS ENUM ('daily', 'weekly', 'monthly', 'total');
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
-
-DO $$ BEGIN
- CREATE TYPE budget_reset_strategy AS ENUM ('calendar', 'rolling', 'anchored');
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
-
-DO $$ BEGIN
- CREATE TYPE budget_on_exceed AS ENUM ('block', 'warn', 'allow');
-EXCEPTION
- WHEN duplicate_object THEN null;
-END $$;
-
-CREATE TABLE IF NOT EXISTS budgets (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- name VARCHAR(120) NOT NULL,
- description TEXT,
- amount NUMERIC(12,4) NOT NULL CHECK (amount >= 0),
- kind budget_kind NOT NULL,
- interval budget_interval NOT NULL,
- reset_strategy budget_reset_strategy NOT NULL DEFAULT 'calendar',
- on_exceed budget_on_exceed NOT NULL DEFAULT 'block',
- is_enabled BOOLEAN NOT NULL DEFAULT true,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-);
-
-CREATE TABLE IF NOT EXISTS budget_assignments (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- budget_id UUID NOT NULL REFERENCES budgets(id) ON DELETE CASCADE,
- team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
- user_id UUID REFERENCES users(id) ON DELETE CASCADE,
- assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- CONSTRAINT budget_assignments_target_check CHECK (
- (team_id IS NOT NULL AND user_id IS NULL) OR
- (team_id IS NULL AND user_id IS NOT NULL)
- )
-);
-
-CREATE UNIQUE INDEX IF NOT EXISTS budget_assignments_team_budget_unique
- ON budget_assignments (team_id, budget_id)
- WHERE team_id IS NOT NULL;
-CREATE UNIQUE INDEX IF NOT EXISTS budget_assignments_user_budget_unique
- ON budget_assignments (user_id, budget_id)
- WHERE user_id IS NOT NULL;
-
-CREATE TABLE IF NOT EXISTS usage_events (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
- model_id UUID REFERENCES models(id) ON DELETE SET NULL,
- provider_id UUID REFERENCES providers(id) ON DELETE SET NULL,
- request_type VARCHAR(50) NOT NULL,
- input_tokens INTEGER NOT NULL DEFAULT 0 CHECK (input_tokens >= 0),
- output_tokens INTEGER NOT NULL DEFAULT 0 CHECK (output_tokens >= 0),
- reasoning_tokens INTEGER NOT NULL DEFAULT 0 CHECK (reasoning_tokens >= 0),
- cost_total NUMERIC(12,6) NOT NULL DEFAULT 0 CHECK (cost_total >= 0),
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-);
-
-CREATE INDEX IF NOT EXISTS idx_usage_events_user_created ON usage_events(user_id, created_at);
-CREATE INDEX IF NOT EXISTS idx_usage_events_model_created ON usage_events(model_id, created_at);
-CREATE INDEX IF NOT EXISTS idx_usage_events_team_created ON usage_events(team_id, created_at);
-
-CREATE TABLE IF NOT EXISTS model_pricing_overrides (
- model_id UUID PRIMARY KEY REFERENCES models(id) ON DELETE CASCADE,
- pricing JSONB NOT NULL CHECK (
- (pricing->>'input') IS NOT NULL AND
- (pricing->>'output') IS NOT NULL AND
- (pricing->>'input')::numeric >= 0 AND
- (pricing->>'output')::numeric >= 0
- ),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-);
-
-INSERT INTO permissions (name, description) VALUES
- ('admin.budgets.view', 'View budget configurations'),
- ('admin.budgets.edit', 'Manage budget configurations'),
- ('admin.analytics.view', 'View usage analytics')
-ON CONFLICT (name) DO NOTHING;
-
-INSERT INTO role_permissions (role_id, permission_id)
-SELECT r.id, p.id FROM roles r, permissions p
-WHERE r.name = 'admin'
- AND p.name IN ('admin.budgets.view', 'admin.budgets.edit', 'admin.analytics.view')
-ON CONFLICT DO NOTHING;
-
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'settings.tabs.budgets', 'Budgets'),
- ('en', 'settings.tabs.analytics', 'Analytics'),
- ('en', 'settings.budgets.description', 'Limit spend for users and teams'),
- ('en', 'settings.budgets.create', 'Create budget'),
- ('en', 'settings.budgets.name', 'Name'),
- ('en', 'settings.budgets.amount', 'Amount'),
- ('en', 'settings.budgets.kind', 'Kind'),
- ('en', 'settings.budgets.interval', 'Interval'),
- ('en', 'settings.budgets.reset_strategy', 'Reset'),
- ('en', 'settings.budgets.on_exceed', 'Action'),
- ('en', 'settings.budgets.enabled', 'Enabled'),
- ('en', 'settings.budgets.assign_team', 'Assign team'),
- ('en', 'settings.budgets.assign_user', 'Assign user'),
- ('en', 'settings.budgets.used', 'Used'),
- ('en', 'settings.budgets.remaining', 'Remaining'),
- ('en', 'settings.budgets.notes', 'Description'),
- ('en', 'settings.budgets.assignments', 'Assignments'),
- ('en', 'settings.budgets.no_assignments', 'No assignments yet'),
- ('en', 'settings.budgets.pick_team', 'Select a team…'),
- ('en', 'settings.budgets.pick_user', 'Select a user…'),
- ('en', 'settings.budgets.reached', 'Budget reached'),
- ('en', 'settings.analytics.total_cost', 'Total cost'),
- ('en', 'settings.analytics.total_tokens', 'Total tokens'),
- ('en', 'settings.analytics.avg_cost', 'Avg. cost / day'),
- ('en', 'settings.analytics.top_models', 'By model'),
- ('en', 'settings.analytics.top_users', 'By user'),
- ('en', 'settings.analytics.top_teams', 'By team'),
- ('en', 'settings.analytics.spend_over_time', 'Spend over time'),
- ('en', 'settings.analytics.token_split', 'Token split'),
- ('en', 'settings.analytics.label', 'Label'),
- ('en', 'settings.analytics.last_7d', 'Last 7 days'),
- ('en', 'settings.analytics.last_30d', 'Last 30 days'),
- ('en', 'settings.analytics.last_90d', 'Last 90 days'),
- ('en', 'settings.analytics.custom', 'Custom'),
- ('en', 'settings.analytics.description', 'Review token and spend trends'),
- ('en', 'settings.analytics.group_by', 'Group by'),
- ('en', 'settings.analytics.from', 'From'),
- ('en', 'settings.analytics.to', 'To'),
- ('en', 'settings.analytics.cost', 'Cost'),
- ('en', 'settings.analytics.tokens', 'Tokens'),
- ('en', 'settings.models.editor.pricing', 'Pricing'),
- ('en', 'settings.models.editor.pricing_desc', 'Override accounting prices per one million tokens.'),
- ('en', 'settings.models.editor.reported_price', 'Reported price'),
- ('en', 'settings.models.editor.effective_price', 'Effective price'),
- ('en', 'settings.models.editor.override_input', 'Input override'),
- ('en', 'settings.models.editor.override_output', 'Output override'),
- ('en', 'settings.models.editor.override_reasoning', 'Reasoning override'),
- ('en', 'settings.models.editor.override_cache_read', 'Cache read override'),
- ('en', 'settings.models.editor.override_cache_write', 'Cache write override'),
- ('en', 'settings.models.editor.pricing_unit', 'Rates are per one million tokens. Leave optional fields empty to keep them unset.'),
- ('en', 'settings.models.editor.pricing_saved', 'Pricing saved'),
- ('en', 'settings.models.editor.pricing_deleted', 'Pricing override removed'),
- ('en', 'sidebar.budget.remaining', '{amount} left'),
- ('de', 'settings.tabs.budgets', 'Budgets'),
- ('de', 'settings.tabs.analytics', 'Analysen'),
- ('de', 'settings.budgets.description', 'Ausgaben fuer Benutzer und Teams begrenzen'),
- ('de', 'settings.budgets.create', 'Budget erstellen'),
- ('de', 'settings.budgets.name', 'Name'),
- ('de', 'settings.budgets.amount', 'Betrag'),
- ('de', 'settings.budgets.kind', 'Art'),
- ('de', 'settings.budgets.interval', 'Intervall'),
- ('de', 'settings.budgets.reset_strategy', 'Zuruecksetzen'),
- ('de', 'settings.budgets.on_exceed', 'Aktion'),
- ('de', 'settings.budgets.enabled', 'Aktiv'),
- ('de', 'settings.budgets.assign_team', 'Team zuweisen'),
- ('de', 'settings.budgets.assign_user', 'Benutzer zuweisen'),
- ('de', 'settings.budgets.used', 'Verbraucht'),
- ('de', 'settings.budgets.remaining', 'Verbleibend'),
- ('de', 'settings.budgets.notes', 'Beschreibung'),
- ('de', 'settings.budgets.assignments', 'Zuweisungen'),
- ('de', 'settings.budgets.no_assignments', 'Noch keine Zuweisungen'),
- ('de', 'settings.budgets.pick_team', 'Team auswaehlen…'),
- ('de', 'settings.budgets.pick_user', 'Benutzer auswaehlen…'),
- ('de', 'settings.budgets.reached', 'Budget erreicht'),
- ('de', 'settings.analytics.total_cost', 'Gesamtkosten'),
- ('de', 'settings.analytics.total_tokens', 'Gesamte Tokens'),
- ('de', 'settings.analytics.avg_cost', 'Durchschn. Kosten / Tag'),
- ('de', 'settings.analytics.top_models', 'Nach Modell'),
- ('de', 'settings.analytics.top_users', 'Nach Benutzer'),
- ('de', 'settings.analytics.top_teams', 'Nach Team'),
- ('de', 'settings.analytics.spend_over_time', 'Ausgaben ueber Zeit'),
- ('de', 'settings.analytics.token_split', 'Token-Aufteilung'),
- ('de', 'settings.analytics.label', 'Bezeichnung'),
- ('de', 'settings.analytics.last_7d', 'Letzte 7 Tage'),
- ('de', 'settings.analytics.last_30d', 'Letzte 30 Tage'),
- ('de', 'settings.analytics.last_90d', 'Letzte 90 Tage'),
- ('de', 'settings.analytics.custom', 'Benutzerdefiniert'),
- ('de', 'settings.analytics.description', 'Token- und Ausgabentrends pruefen'),
- ('de', 'settings.analytics.group_by', 'Gruppieren nach'),
- ('de', 'settings.analytics.from', 'Von'),
- ('de', 'settings.analytics.to', 'Bis'),
- ('de', 'settings.analytics.cost', 'Kosten'),
- ('de', 'settings.analytics.tokens', 'Tokens'),
- ('de', 'settings.models.editor.pricing', 'Preise'),
- ('de', 'settings.models.editor.pricing_desc', 'Abrechnungspreise pro eine Million Tokens ueberschreiben.'),
- ('de', 'settings.models.editor.reported_price', 'Gemeldeter Preis'),
- ('de', 'settings.models.editor.effective_price', 'Effektiver Preis'),
- ('de', 'settings.models.editor.override_input', 'Eingabe-Ueberschreibung'),
- ('de', 'settings.models.editor.override_output', 'Ausgabe-Ueberschreibung'),
- ('de', 'settings.models.editor.override_reasoning', 'Reasoning-Ueberschreibung'),
- ('de', 'settings.models.editor.override_cache_read', 'Cache-Lese-Ueberschreibung'),
- ('de', 'settings.models.editor.override_cache_write', 'Cache-Schreib-Ueberschreibung'),
- ('de', 'settings.models.editor.pricing_unit', 'Preise gelten pro eine Million Tokens. Optionale Felder leer lassen, um sie nicht zu setzen.'),
- ('de', 'settings.models.editor.pricing_saved', 'Preise gespeichert'),
- ('de', 'settings.models.editor.pricing_deleted', 'Preis-Ueberschreibung entfernt'),
- ('de', 'sidebar.budget.remaining', '{amount} uebrig')
-ON CONFLICT (language, key_path) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW();
-
-INSERT INTO i18n_translations (language, key_path, value)
-VALUES
- ('en', 'settings.analytics.my_usage', 'My Usage'),
- ('en', 'settings.analytics.requests', 'Requests'),
- ('en', 'settings.analytics.per_day', 'per day'),
- ('en', 'settings.analytics.usage_by_model', 'Usage by model'),
- ('en', 'settings.analytics.request_volume', 'Request volume by model'),
- ('en', 'settings.analytics.no_data', 'No data available for this period'),
- ('en', 'settings.analytics.trending', 'Trending'),
- ('en', 'settings.analytics.tab_overview', 'Overview'),
- ('en', 'settings.analytics.tab_trends', 'Trends'),
- ('en', 'settings.analytics.tab_explore', 'Explore'),
- ('en', 'settings.analytics.metric', 'Metric'),
- ('en', 'settings.analytics.model', 'Model'),
- ('en', 'settings.analytics.none', 'None'),
- ('en', 'settings.analytics.value', 'Value'),
- ('en', 'settings.analytics.tokens_input', 'Tokens (Input)'),
- ('en', 'settings.analytics.tokens_output', 'Tokens (Output)'),
- ('en', 'settings.analytics.tokens_reasoning', 'Tokens (Reasoning)'),
- ('de', 'settings.analytics.my_usage', 'Meine Nutzung'),
- ('de', 'settings.analytics.requests', 'Anfragen'),
- ('de', 'settings.analytics.per_day', 'pro Tag'),
- ('de', 'settings.analytics.usage_by_model', 'Nutzung nach Modell'),
- ('de', 'settings.analytics.request_volume', 'Anfrageanzahl nach Modell'),
- ('de', 'settings.analytics.no_data', 'Keine Daten fuer diesen Zeitraum verfuegbar'),
- ('de', 'settings.analytics.trending', 'Trend'),
- ('de', 'settings.analytics.tab_overview', 'Uebersicht'),
- ('de', 'settings.analytics.tab_trends', 'Trends'),
- ('de', 'settings.analytics.tab_explore', 'Erkunden'),
- ('de', 'settings.analytics.metric', 'Metrik'),
- ('de', 'settings.analytics.model', 'Modell'),
- ('de', 'settings.analytics.none', 'Keine'),
- ('de', 'settings.analytics.value', 'Wert'),
- ('de', 'settings.analytics.tokens_input', 'Tokens (Eingabe)'),
- ('de', 'settings.analytics.tokens_output', 'Tokens (Ausgabe)'),
- ('de', 'settings.analytics.tokens_reasoning', 'Tokens (Reasoning)')
-ON CONFLICT (language, key_path) DO UPDATE SET value = EXCLUDED.value;
-
-INSERT INTO i18n_translations (language, key_path, value)
-VALUES
- ('en', 'settings.analytics.top_api_keys', 'Top API Keys'),
- ('en', 'settings.analytics.coming_soon', 'Coming soon'),
- ('en', 'settings.analytics.api_keys_soon', 'API key tracking will be available in a future release'),
- ('en', 'settings.analytics.total_tokens_short', 'Tokens'),
- ('en', 'settings.analytics.custom_range', 'Custom range'),
- ('en', 'settings.analytics.preset_last_7d', 'Last 7 days'),
- ('en', 'settings.analytics.preset_last_30d', 'Last 30 days'),
- ('en', 'settings.analytics.preset_last_90d', 'Last 90 days'),
- ('en', 'settings.analytics.preset_this_month', 'This month'),
- ('en', 'settings.analytics.preset_last_month', 'Last month'),
- ('en', 'settings.analytics.preset_this_year', 'This year'),
- ('de', 'settings.analytics.top_api_keys', 'Top API-Schlüssel'),
- ('de', 'settings.analytics.coming_soon', 'Demnächst'),
- ('de', 'settings.analytics.api_keys_soon', 'API-Schlüssel-Tracking wird in einem zukünftigen Release verfügbar sein'),
- ('de', 'settings.analytics.total_tokens_short', 'Tokens'),
- ('de', 'settings.analytics.custom_range', 'Benutzerdefinierter Zeitraum'),
- ('de', 'settings.analytics.preset_last_7d', 'Letzte 7 Tage'),
- ('de', 'settings.analytics.preset_last_30d', 'Letzte 30 Tage'),
- ('de', 'settings.analytics.preset_last_90d', 'Letzte 90 Tage'),
- ('de', 'settings.analytics.preset_this_month', 'Diesen Monat'),
- ('de', 'settings.analytics.preset_last_month', 'Letzten Monat'),
- ('de', 'settings.analytics.preset_this_year', 'Dieses Jahr')
-ON CONFLICT (language, key_path) DO UPDATE SET value = EXCLUDED.value;
-
-INSERT INTO i18n_translations (language, key_path, value)
-VALUES
- ('en', 'settings.analytics.spend', 'Spend'),
- ('en', 'settings.analytics.section_models', 'Models'),
- ('en', 'settings.analytics.section_api_keys', 'API Keys'),
- ('en', 'settings.analytics.section_user', 'Users'),
- ('en', 'settings.analytics.trend', 'Trend'),
- ('de', 'settings.analytics.spend', 'Ausgaben'),
- ('de', 'settings.analytics.section_models', 'Modelle'),
- ('de', 'settings.analytics.section_api_keys', 'API-Schlüssel'),
- ('de', 'settings.analytics.section_user', 'Benutzer'),
- ('de', 'settings.analytics.trend', 'Trend')
-ON CONFLICT (language, key_path) DO UPDATE SET value = EXCLUDED.value;
-
-INSERT INTO i18n_translations (language, key_path, value)
-VALUES
- ('en', 'settings.analytics.request_count', 'Request count'),
- ('en', 'settings.analytics.total_usage_dollars', 'Total usage ($)'),
- ('en', 'settings.analytics.tokens_total', 'Tokens (total)'),
- ('en', 'settings.analytics.tokens_prompt', 'Tokens (prompt)'),
- ('en', 'settings.analytics.tokens_completion', 'Tokens (completion)'),
- ('en', 'settings.analytics.reasoning_tokens', 'Reasoning tokens'),
- ('en', 'settings.analytics.latency', 'Latency'),
- ('en', 'settings.analytics.api_key', 'API key'),
- ('en', 'settings.analytics.provider', 'Provider'),
- ('en', 'settings.analytics.user', 'User'),
- ('en', 'settings.analytics.top', 'Top'),
- ('en', 'settings.analytics.rollup', 'Rollup'),
- ('en', 'settings.analytics.hourly', 'Hourly'),
- ('en', 'settings.analytics.daily', 'Daily'),
- ('en', 'settings.analytics.weekly', 'Weekly'),
- ('en', 'settings.analytics.total', 'Total'),
- ('en', 'settings.analytics.min', 'Min'),
- ('en', 'settings.analytics.max', 'Max'),
- ('en', 'settings.analytics.avg', 'Avg'),
- ('en', 'settings.analytics.sum', 'Sum'),
- ('en', 'settings.analytics.other', 'Other'),
- ('de', 'settings.analytics.request_count', 'Anfragen'),
- ('de', 'settings.analytics.total_usage_dollars', 'Gesamtnutzung ($)'),
- ('de', 'settings.analytics.tokens_total', 'Tokens (gesamt)'),
- ('de', 'settings.analytics.tokens_prompt', 'Tokens (Prompt)'),
- ('de', 'settings.analytics.tokens_completion', 'Tokens (Completion)'),
- ('de', 'settings.analytics.reasoning_tokens', 'Reasoning-Tokens'),
- ('de', 'settings.analytics.latency', 'Latenz'),
- ('de', 'settings.analytics.api_key', 'API-Schluessel'),
- ('de', 'settings.analytics.provider', 'Provider'),
- ('de', 'settings.analytics.user', 'Benutzer'),
- ('de', 'settings.analytics.top', 'Top'),
- ('de', 'settings.analytics.rollup', 'Rollup'),
- ('de', 'settings.analytics.hourly', 'Stuendlich'),
- ('de', 'settings.analytics.daily', 'Taeglich'),
- ('de', 'settings.analytics.weekly', 'Woechentlich'),
- ('de', 'settings.analytics.total', 'Gesamt'),
- ('de', 'settings.analytics.min', 'Min'),
- ('de', 'settings.analytics.max', 'Max'),
- ('de', 'settings.analytics.avg', 'Durchschn.'),
- ('de', 'settings.analytics.sum', 'Summe'),
- ('de', 'settings.analytics.other', 'Andere')
-ON CONFLICT (language, key_path) DO UPDATE SET value = EXCLUDED.value;
diff --git a/migrations/20260706010000_budget_manual_resets.sql b/migrations/20260706010000_budget_manual_resets.sql
deleted file mode 100644
index e6ced2d..0000000
--- a/migrations/20260706010000_budget_manual_resets.sql
+++ /dev/null
@@ -1,117 +0,0 @@
-CREATE TABLE IF NOT EXISTS budget_reset_events (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- assignment_id UUID REFERENCES budget_assignments(id) ON DELETE CASCADE,
- budget_id UUID REFERENCES budgets(id) ON DELETE CASCADE,
- team_id UUID REFERENCES teams(id) ON DELETE CASCADE,
- user_id UUID REFERENCES users(id) ON DELETE CASCADE,
- kind budget_kind,
- reason TEXT,
- reset_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- created_by UUID REFERENCES users(id) ON DELETE SET NULL,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- CONSTRAINT budget_reset_events_scope_check CHECK (
- num_nonnulls(assignment_id, budget_id, team_id, user_id) = 1
- )
-);
-
-CREATE INDEX IF NOT EXISTS idx_budget_reset_events_assignment ON budget_reset_events(assignment_id, reset_at);
-CREATE INDEX IF NOT EXISTS idx_budget_reset_events_budget ON budget_reset_events(budget_id, reset_at);
-CREATE INDEX IF NOT EXISTS idx_budget_reset_events_team ON budget_reset_events(team_id, reset_at);
-CREATE INDEX IF NOT EXISTS idx_budget_reset_events_user ON budget_reset_events(user_id, reset_at);
-
-INSERT INTO i18n_translations (language, key_path, value) VALUES
- ('en', 'common.refresh', 'Refresh'),
- ('en', 'settings.budgets.tab_users', 'Users'),
- ('en', 'settings.budgets.tab_teams', 'Teams'),
- ('en', 'settings.budgets.tab_budgets', 'Budgets'),
- ('en', 'settings.budgets.tab_history', 'History'),
- ('en', 'settings.budgets.search_users', 'Search users'),
- ('en', 'settings.budgets.search_teams', 'Search teams'),
- ('en', 'settings.budgets.users_with_budgets', 'users'),
- ('en', 'settings.budgets.user', 'User'),
- ('en', 'settings.budgets.team', 'team'),
- ('en', 'settings.budgets.teams', 'Teams'),
- ('en', 'settings.budgets.members', 'members'),
- ('en', 'settings.budgets.effective_budgets', 'Effective budgets'),
- ('en', 'settings.budgets.status', 'Status'),
- ('en', 'settings.budgets.budget_count', 'budgets'),
- ('en', 'settings.budgets.no_user_budgets', 'No user budgets found'),
- ('en', 'settings.budgets.no_team_budgets', 'No team budgets found'),
- ('en', 'settings.budgets.no_reset', 'No reset'),
- ('en', 'settings.budgets.reset', 'Reset'),
- ('en', 'settings.budgets.affected_users', 'affected'),
- ('en', 'settings.budgets.settings', 'Settings'),
- ('en', 'settings.budgets.team_budget', 'team budget'),
- ('en', 'settings.budgets.user_budget', 'user budget'),
- ('en', 'settings.budgets.reset_at', 'Reset at'),
- ('en', 'settings.budgets.scope', 'Scope'),
- ('en', 'settings.budgets.budget', 'Budget'),
- ('en', 'settings.budgets.reason', 'Reason'),
- ('en', 'settings.budgets.reset_by', 'Reset by'),
- ('en', 'settings.budgets.no_reset_history', 'No reset history yet'),
- ('en', 'settings.budgets.reset_budget', 'Reset budget'),
- ('en', 'settings.budgets.kind_scope', 'Kind'),
- ('en', 'settings.budgets.all_kinds', 'All kinds'),
- ('en', 'settings.budgets.reason_placeholder', 'Optional note for the audit log'),
- ('en', 'settings.budgets.reset_keeps_history', 'Usage history is kept. This starts budget accounting from now for the selected scope.'),
- ('en', 'settings.budgets.assignment', 'Assignment'),
- ('en', 'settings.budgets.create_success', 'Budget created'),
- ('en', 'settings.budgets.create_error', 'Failed to create budget'),
- ('en', 'settings.budgets.save_success', 'Budget saved'),
- ('en', 'settings.budgets.save_error', 'Failed to save budget'),
- ('en', 'settings.budgets.delete_success', 'Budget deleted'),
- ('en', 'settings.budgets.delete_error', 'Failed to delete budget'),
- ('en', 'settings.budgets.assign_success', 'Budget assigned'),
- ('en', 'settings.budgets.assign_error', 'Failed to assign budget'),
- ('en', 'settings.budgets.remove_success', 'Assignment removed'),
- ('en', 'settings.budgets.remove_error', 'Failed to remove assignment'),
- ('en', 'settings.budgets.reset_success', 'Budget reset'),
- ('en', 'settings.budgets.reset_error', 'Failed to reset budget'),
- ('de', 'common.refresh', 'Aktualisieren'),
- ('de', 'settings.budgets.tab_users', 'Benutzer'),
- ('de', 'settings.budgets.tab_teams', 'Teams'),
- ('de', 'settings.budgets.tab_budgets', 'Budgets'),
- ('de', 'settings.budgets.tab_history', 'Historie'),
- ('de', 'settings.budgets.search_users', 'Benutzer suchen'),
- ('de', 'settings.budgets.search_teams', 'Teams suchen'),
- ('de', 'settings.budgets.users_with_budgets', 'Benutzer'),
- ('de', 'settings.budgets.user', 'Benutzer'),
- ('de', 'settings.budgets.team', 'Team'),
- ('de', 'settings.budgets.teams', 'Teams'),
- ('de', 'settings.budgets.members', 'Mitglieder'),
- ('de', 'settings.budgets.effective_budgets', 'Wirksame Budgets'),
- ('de', 'settings.budgets.status', 'Status'),
- ('de', 'settings.budgets.budget_count', 'Budgets'),
- ('de', 'settings.budgets.no_user_budgets', 'Keine Benutzerbudgets gefunden'),
- ('de', 'settings.budgets.no_team_budgets', 'Keine Teambudgets gefunden'),
- ('de', 'settings.budgets.no_reset', 'Kein Reset'),
- ('de', 'settings.budgets.reset', 'Zuruecksetzen'),
- ('de', 'settings.budgets.affected_users', 'betroffen'),
- ('de', 'settings.budgets.settings', 'Einstellungen'),
- ('de', 'settings.budgets.team_budget', 'Teambudget'),
- ('de', 'settings.budgets.user_budget', 'Benutzerbudget'),
- ('de', 'settings.budgets.reset_at', 'Reset um'),
- ('de', 'settings.budgets.scope', 'Bereich'),
- ('de', 'settings.budgets.budget', 'Budget'),
- ('de', 'settings.budgets.reason', 'Grund'),
- ('de', 'settings.budgets.reset_by', 'Zurueckgesetzt von'),
- ('de', 'settings.budgets.no_reset_history', 'Noch keine Reset-Historie'),
- ('de', 'settings.budgets.reset_budget', 'Budget zuruecksetzen'),
- ('de', 'settings.budgets.kind_scope', 'Art'),
- ('de', 'settings.budgets.all_kinds', 'Alle Arten'),
- ('de', 'settings.budgets.reason_placeholder', 'Optionale Notiz fuer das Audit-Log'),
- ('de', 'settings.budgets.reset_keeps_history', 'Die Nutzungshistorie bleibt erhalten. Die Budgetberechnung startet fuer diesen Bereich ab jetzt neu.'),
- ('de', 'settings.budgets.assignment', 'Zuweisung'),
- ('de', 'settings.budgets.create_success', 'Budget erstellt'),
- ('de', 'settings.budgets.create_error', 'Budget konnte nicht erstellt werden'),
- ('de', 'settings.budgets.save_success', 'Budget gespeichert'),
- ('de', 'settings.budgets.save_error', 'Budget konnte nicht gespeichert werden'),
- ('de', 'settings.budgets.delete_success', 'Budget geloescht'),
- ('de', 'settings.budgets.delete_error', 'Budget konnte nicht geloescht werden'),
- ('de', 'settings.budgets.assign_success', 'Budget zugewiesen'),
- ('de', 'settings.budgets.assign_error', 'Budget konnte nicht zugewiesen werden'),
- ('de', 'settings.budgets.remove_success', 'Zuweisung entfernt'),
- ('de', 'settings.budgets.remove_error', 'Zuweisung konnte nicht entfernt werden'),
- ('de', 'settings.budgets.reset_success', 'Budget zurueckgesetzt'),
- ('de', 'settings.budgets.reset_error', 'Budget konnte nicht zurueckgesetzt werden')
-ON CONFLICT (language, key_path) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW();
diff --git a/migrations/20260711000000_provider_backed_image_tool.sql b/migrations/20260711000000_provider_backed_image_tool.sql
deleted file mode 100644
index 8fd3664..0000000
--- a/migrations/20260711000000_provider_backed_image_tool.sql
+++ /dev/null
@@ -1,138 +0,0 @@
-UPDATE tools
-SET settings_schema = jsonb_build_object(
- 'type', 'object',
- 'required', jsonb_build_array('image_model_id'),
- 'properties', jsonb_build_object(
- 'image_model_id', jsonb_build_object(
- 'type', 'string',
- 'title', 'Image model',
- 'format', 'model-picker',
- 'description', 'Select an enabled provider-backed image model'
- )
- )
-)
-WHERE source_kind = 'BUILTIN' AND source_config->>'builtin_id' = 'imagegen';
-
-WITH candidate_models AS (
- SELECT DISTINCT ON (uts.id)
- uts.id AS settings_id,
- m.id AS model_id
- FROM user_tool_settings uts
- JOIN tools t ON t.id = uts.tool_id
- JOIN providers p ON (
- (LOWER(COALESCE(uts.settings->>'provider', 'openai')) = 'openai' AND p.kind = 'OPENAI')
- OR (LOWER(uts.settings->>'provider') = 'google' AND p.kind = 'GOOGLE')
- )
- JOIN models m ON m.provider_id = p.id
- AND m.model_id = COALESCE(
- uts.settings->>'model',
- CASE LOWER(COALESCE(uts.settings->>'provider', 'openai'))
- WHEN 'google' THEN 'imagen-3.0-generate-002'
- ELSE 'dall-e-3'
- END
- )
- WHERE t.source_kind = 'BUILTIN'
- AND t.source_config->>'builtin_id' = 'imagegen'
- AND NOT uts.settings ? 'image_model_id'
- AND p.is_enabled = true
- AND m.is_enabled = true
- AND EXISTS (
- SELECT 1
- FROM jsonb_array_elements_text(COALESCE(m.output_modalities, '[]'::jsonb)) AS modality(value)
- WHERE LOWER(modality.value) = 'image'
- )
- ORDER BY uts.id, p.id, m.id
-)
-UPDATE user_tool_settings uts
-SET settings = uts.settings || jsonb_build_object('image_model_id', candidate_models.model_id::text)
-FROM candidate_models
-WHERE uts.id = candidate_models.settings_id;
-
-DELETE FROM user_tool_settings uts
-USING tools t
-WHERE uts.tool_id = t.id
- AND t.source_kind = 'BUILTIN'
- AND t.source_config->>'builtin_id' = 'imagegen'
- AND NOT uts.settings ? 'image_model_id';
-
-ALTER TABLE images ADD COLUMN IF NOT EXISTS caption TEXT;
-ALTER TABLE tools ADD COLUMN IF NOT EXISTS system_prompt TEXT;
-
-UPDATE tools
-SET system_prompt = 'Generated images are displayed to the user automatically by the interface. In your replies to the user, never write image URLs, markdown image tags (), or
tags — refer to images in words only (for example, "I''ve generated the image above"). To edit or reference a previously generated image, never invent a URL: call imagegen_edit with the image_id shown for that image in the conversation.'
-WHERE source_kind = 'BUILTIN' AND source_config->>'builtin_id' = 'imagegen' AND system_prompt IS NULL;
-
-INSERT INTO i18n_translations (language, key_path, value)
-VALUES
- ('en', 'settings.tools.image_model_title', 'Image model'),
- ('en', 'settings.tools.image_model_description', 'Select the global image model used by this tool.'),
- ('en', 'settings.tools.image_model_placeholder', 'Select image model'),
- ('en', 'settings.tools.system_prompt', 'System prompt injection'),
- ('en', 'settings.tools.system_prompt_placeholder', 'Extra system instructions added when this tool is active'),
- ('en', 'settings.tools.system_prompt_hint', 'Prepended to the system prompt whenever this tool is enabled for a request. Use it to guide how the model uses the tool.'),
- ('en', 'settings.tools.imagegen.display_name', 'Image Generation'),
- ('en', 'settings.tools.imagegen.description', 'Generate and edit images using an admin-selected provider model'),
- ('en', 'settings.tools.imagegen.generate.description', 'Generate an image from a text prompt'),
- ('en', 'settings.tools.imagegen.generate.prompt', 'The text prompt describing the image to generate'),
- ('en', 'settings.tools.imagegen.generate.size', 'Image size'),
- ('en', 'settings.tools.imagegen.generate.quality', 'Image quality'),
- ('en', 'settings.tools.imagegen.edit.description', 'Edit a previously generated image using a text prompt'),
- ('en', 'settings.tools.imagegen.edit.image_id', 'The image ID of a previously generated image to edit. Preferred over an image URL.'),
- ('en', 'settings.tools.imagegen.edit.image_url', 'URL of an external image to edit. Only use when no image ID is available.'),
- ('en', 'settings.tools.imagegen.edit.prompt', 'The text prompt describing the desired edit'),
- ('en', 'settings.tools.imagegen.settings.description', 'Select an enabled image model'),
- ('de', 'settings.tools.image_model_title', 'Bildmodell'),
- ('de', 'settings.tools.image_model_description', 'Wählen Sie das globale Bildmodell aus, das dieses Tool verwendet.'),
- ('de', 'settings.tools.image_model_placeholder', 'Bildmodell auswählen'),
- ('de', 'settings.tools.system_prompt', 'System-Prompt-Einschub'),
- ('de', 'settings.tools.system_prompt_placeholder', 'Zusätzliche Systemanweisungen, wenn dieses Tool aktiv ist'),
- ('de', 'settings.tools.system_prompt_hint', 'Wird dem System-Prompt vorangestellt, sobald dieses Tool für eine Anfrage aktiviert ist. Damit steuern Sie, wie das Modell das Tool verwendet.'),
- ('de', 'settings.tools.imagegen.display_name', 'Bildgenerierung'),
- ('de', 'settings.tools.imagegen.description', 'Bilder mit einem von der Administration ausgewählten Anbietermodell erstellen und bearbeiten'),
- ('de', 'settings.tools.imagegen.generate.description', 'Ein Bild anhand einer Texteingabe erstellen'),
- ('de', 'settings.tools.imagegen.generate.prompt', 'Die Texteingabe, die das zu erstellende Bild beschreibt'),
- ('de', 'settings.tools.imagegen.generate.size', 'Bildgröße'),
- ('de', 'settings.tools.imagegen.generate.quality', 'Bildqualität'),
- ('de', 'settings.tools.imagegen.edit.description', 'Ein zuvor erstelltes Bild anhand einer Texteingabe bearbeiten'),
- ('de', 'settings.tools.imagegen.edit.image_id', 'Die Bild-ID eines zuvor erstellten Bildes. Diese wird gegenüber einer Bild-URL bevorzugt.'),
- ('de', 'settings.tools.imagegen.edit.image_url', 'URL eines externen Bildes. Nur verwenden, wenn keine Bild-ID verfügbar ist.'),
- ('de', 'settings.tools.imagegen.edit.prompt', 'Die Texteingabe, die die gewünschte Bearbeitung beschreibt'),
- ('de', 'settings.tools.imagegen.settings.description', 'Ein aktiviertes Bildmodell auswählen')
-ON CONFLICT (language, key_path) DO UPDATE SET value = EXCLUDED.value;
-
-UPDATE tool_functions tf
-SET input_schema = jsonb_build_object(
- 'type', 'object',
- 'required', jsonb_build_array('prompt'),
- 'properties', jsonb_build_object(
- 'image_id', jsonb_build_object(
- 'type', 'string',
- 'description', 'The image_id of a previously generated image to edit (shown in the conversation). Preferred over image_url.'
- ),
- 'image_url', jsonb_build_object(
- 'type', 'string',
- 'description', 'URL of an external image to edit. Only use when no image_id is available.'
- ),
- 'prompt', jsonb_build_object(
- 'type', 'string',
- 'description', 'The text prompt describing the desired edit'
- )
- )
-)
-FROM tools t
-WHERE tf.tool_id = t.id
- AND t.source_kind = 'BUILTIN'
- AND t.source_config->>'builtin_id' = 'imagegen'
- AND tf.name = 'edit';
-
-UPDATE tool_functions tf
-SET input_schema = jsonb_set(
- jsonb_set(tf.input_schema, '{properties,quality,enum}', '["auto", "low", "medium", "high"]'::jsonb, true),
- '{properties,quality,default}', '"auto"'::jsonb, true
-)
-FROM tools t
-WHERE tf.tool_id = t.id
- AND t.source_kind = 'BUILTIN'
- AND t.source_config->>'builtin_id' = 'imagegen'
- AND tf.name = 'generate'
- AND tf.input_schema -> 'properties' ? 'quality';
diff --git a/src/ai.rs b/src/ai.rs
index d714cea..c6300c5 100644
--- a/src/ai.rs
+++ b/src/ai.rs
@@ -25,15 +25,17 @@ pub async fn init(pool: &PgPool) {
let engine = Arc::new(RwLock::new(OmniferenceEngine::new()));
let providers = Provider::list_enabled_system(pool).await.unwrap_or_default();
- if providers.is_empty() {
- println!("[AI] No providers found in the database.");
- return;
- }
let provider_count = providers.len();
let mut engine_write = engine.write().await;
for provider in providers {
- let config = provider_to_config(&provider);
+ let config = match provider_to_config(&provider) {
+ Ok(config) => config,
+ Err(error) => {
+ tracing::error!(provider_id=%provider.id, "Failed to decrypt provider credential: {error}");
+ continue;
+ }
+ };
if let Err(e) = engine_write.register_provider(config).await {
eprintln!("[AI] Failed to register provider '{}': {}", provider.name, e);
@@ -59,7 +61,7 @@ pub fn get() -> Arc> {
pub async fn catalog() -> Option> {
let engine = OF_ENGINE.get()?;
let guard = engine.read().await;
- Some(guard.service().catalog.clone())
+ Some(guard.service().catalog().clone())
}
/// Pricing overrides are stored in the database and applied at usage-event
@@ -79,7 +81,13 @@ pub async fn reload_providers(pool: &PgPool) {
*engine_write = new_engine;
for provider in providers {
- let config = provider_to_config(&provider);
+ let config = match provider_to_config(&provider) {
+ Ok(config) => config,
+ Err(error) => {
+ tracing::error!(provider_id=%provider.id, "Failed to decrypt provider credential: {error}");
+ continue;
+ }
+ };
if let Err(e) = engine_write.register_provider(config).await {
eprintln!("[AI] Failed to register provider '{}': {}", provider.name, e);
}
@@ -90,11 +98,11 @@ pub async fn reload_providers(pool: &PgPool) {
}
/// Convert a database provider to omniference config
-pub fn provider_to_config(provider: &Provider) -> ProviderConfig {
- let api_key = provider.api_key.as_ref().map(|k| decrypt_api_key(k));
- let extra_headers = parse_extra_headers(&provider.extra_headers.0);
+pub fn provider_to_config(provider: &Provider) -> Result {
+ let api_key = provider.api_key.as_deref().map(decrypt_api_key).transpose()?;
+ let extra_headers = parse_extra_headers(&provider.extra_headers.0)?;
- ProviderConfig {
+ Ok(ProviderConfig {
name: provider.name.clone(),
endpoint: ProviderEndpoint {
kind: provider.kind.to_omni_kind(),
@@ -105,13 +113,16 @@ pub fn provider_to_config(provider: &Provider) -> ProviderConfig {
},
enabled: provider.is_enabled,
catalog_provider_slug: None,
- }
+ })
}
-pub fn parse_extra_headers(value: &serde_json::Value) -> BTreeMap {
+pub fn parse_extra_headers(value: &serde_json::Value) -> Result, crate::utils::encryption::EncryptionError> {
if let Some(obj) = value.as_object() {
- obj.iter().filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))).collect()
+ obj.iter()
+ .filter_map(|(key, value)| value.as_str().map(|secret| (key, secret)))
+ .map(|(key, secret)| crate::utils::encryption::decrypt_api_key(secret).map(|value| (key.clone(), value)))
+ .collect()
} else {
- BTreeMap::new()
+ Ok(BTreeMap::new())
}
}
diff --git a/src/config.rs b/src/config.rs
index bf5ae8c..5639ff1 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -7,9 +7,11 @@
use crate::i18n::Language;
use crate::types::ThemeCssVars;
+use crate::utils::encryption::{decrypt_api_key, encrypt_secret_value};
use arc_swap::ArcSwap;
use sqlx::PgPool;
use std::sync::{Arc, OnceLock};
+use uuid::Uuid;
/// Global config instance
static CONFIG: OnceLock = OnceLock::new();
@@ -71,8 +73,8 @@ pub struct ConfigValues {
/// When enabled, admins may register global MCP servers that run over
/// server-side `stdio` (arbitrary commands on the host). Off by default.
pub allow_server_stdio_mcp: bool,
- /// Instance-wide default model key. Applied when neither user nor team default is set.
- pub default_model_key: Option,
+ /// Instance-wide default model. Applied when neither user nor team default is set.
+ pub default_model_id: Option,
}
impl Default for ConfigValues {
@@ -86,7 +88,7 @@ impl Default for ConfigValues {
oauth_discord_client_secret: None,
enable_provider_selector: false,
allow_server_stdio_mcp: false,
- default_model_key: None,
+ default_model_id: None,
}
}
}
@@ -160,10 +162,10 @@ impl Config {
self.values.load().allow_server_stdio_mcp
}
- /// Get the instance-wide default model key, if configured.
+ /// Get the instance-wide default model ID, if configured.
#[must_use]
- pub fn default_model_key(&self) -> Option {
- self.values.load().default_model_key.clone()
+ pub fn default_model_id(&self) -> Option {
+ self.values.load().default_model_id
}
/// Get current configuration values.
@@ -241,15 +243,15 @@ impl Config {
Ok(())
}
- /// Set or clear the instance-wide default model key in the database and reload config.
+ /// Set or clear the instance-wide default model ID in the database and reload config.
///
/// # Errors
/// Returns an error if the database write fails.
- pub async fn set_default_model_key(&self, pool: &PgPool, key: Option<&str>) -> Result<(), sqlx::Error> {
- match key {
- Some(k) => Self::upsert_config(pool, "default_model_key", k).await?,
+ pub async fn set_default_model_id(&self, pool: &PgPool, id: Option) -> Result<(), sqlx::Error> {
+ match id {
+ Some(id) => Self::upsert_config(pool, "default_model_id", &id.to_string()).await?,
None => {
- sqlx::query("DELETE FROM app_config WHERE key = 'default_model_key'").execute(pool).await?;
+ sqlx::query("DELETE FROM app_config WHERE key = 'default_model_id'").execute(pool).await?;
}
}
self.reload(pool).await;
@@ -257,6 +259,13 @@ impl Config {
}
async fn upsert_config(pool: &PgPool, key: &str, value: &str) -> Result<(), sqlx::Error> {
+ let protected;
+ let value = if matches!(key, "oauth_google_client_secret" | "oauth_discord_client_secret") {
+ protected = encrypt_secret_value(value, None).map_err(|error| sqlx::Error::Protocol(error.to_string()))?;
+ protected.as_str()
+ } else {
+ value
+ };
sqlx::query(
r#"
INSERT INTO app_config (key, value)
@@ -286,12 +295,18 @@ impl Config {
}
}
"oauth_google_client_id" => values.oauth_google_client_id = Some(row.value),
- "oauth_google_client_secret" => values.oauth_google_client_secret = Some(row.value),
+ "oauth_google_client_secret" => match decrypt_api_key(&row.value) {
+ Ok(value) => values.oauth_google_client_secret = Some(value),
+ Err(error) => tracing::error!("Failed to decrypt Google OAuth client secret: {error}"),
+ },
"oauth_discord_client_id" => values.oauth_discord_client_id = Some(row.value),
- "oauth_discord_client_secret" => values.oauth_discord_client_secret = Some(row.value),
+ "oauth_discord_client_secret" => match decrypt_api_key(&row.value) {
+ Ok(value) => values.oauth_discord_client_secret = Some(value),
+ Err(error) => tracing::error!("Failed to decrypt Discord OAuth client secret: {error}"),
+ },
"enable_provider_selector" => values.enable_provider_selector = row.value == "true",
"allow_server_stdio_mcp" => values.allow_server_stdio_mcp = row.value == "true",
- "default_model_key" => values.default_model_key = Some(row.value),
+ "default_model_id" => values.default_model_id = Uuid::parse_str(&row.value).ok(),
_ => {} // Ignore unknown config keys for forward compatibility
}
}
diff --git a/src/jobs.rs b/src/jobs.rs
index c3117ad..03cff04 100644
--- a/src/jobs.rs
+++ b/src/jobs.rs
@@ -4,7 +4,9 @@
use crate::types::providers::Provider;
use crate::types::tools::McpServer;
+use crate::utils::provider_billing::refresh_provider_billing;
use crate::utils::providers::sync_provider_models;
+use futures_util::{StreamExt, stream};
use sqlx::PgPool;
use std::sync::Arc;
use std::time::Duration;
@@ -12,6 +14,7 @@ use std::time::Duration;
/// Minimum interval between session cleanup runs.
const SESSION_CLEANUP_INTERVAL_SECS: u64 = 3600; // 1 hour
const PROVIDER_SYNC_INTERVAL_SECS: u64 = 500; // 5 minutes
+const PROVIDER_BILLING_INTERVAL_SECS: u64 = 900; // 15 minutes
/// How often to reap idle MCP clients and health-check system servers.
const MCP_MAINTENANCE_INTERVAL_SECS: u64 = 300; // 5 minutes
/// Close pooled MCP clients that have been idle for longer than this.
@@ -26,6 +29,7 @@ pub async fn start_job_scheduler(state: Arc) {
let handles: Vec> = vec![
tokio::spawn(session_cleanup_job(Arc::clone(&state))),
tokio::spawn(provider_sync_job(Arc::clone(&state))),
+ tokio::spawn(provider_billing_job(Arc::clone(&state))),
tokio::spawn(mcp_maintenance_job(Arc::clone(&state))),
];
@@ -34,6 +38,33 @@ pub async fn start_job_scheduler(state: Arc) {
}
}
+async fn provider_billing_job(state: Arc) {
+ tracing::info!("[JOBS] Provider billing refresh job started");
+ loop {
+ tokio::time::sleep(Duration::from_secs(PROVIDER_BILLING_INTERVAL_SECS)).await;
+ let providers = match Provider::list_enabled_billing_connections(&state.db).await {
+ Ok(providers) => providers,
+ Err(error) => {
+ tracing::error!("[JOBS] Failed to list provider billing connections: {error}");
+ continue;
+ }
+ };
+ stream::iter(providers)
+ .for_each_concurrent(4, |provider| {
+ let state = &state;
+ async move {
+ match refresh_provider_billing(&state.db, &provider).await {
+ Ok(_) => tracing::info!(provider_id=%provider.id, provider_name=%provider.name, status="AVAILABLE", "[JOBS] Refreshed provider billing"),
+ Err(error) => {
+ tracing::warn!(provider_id=%provider.id, provider_name=%provider.name, category=error.code(), "[JOBS] Provider billing refresh failed")
+ }
+ }
+ }
+ })
+ .await;
+ }
+}
+
/// Sync models for every enabled system provider once.
pub async fn sync_provider_models_once(state: &super::JobState) {
match Provider::list_enabled_system(&state.db).await {
@@ -43,7 +74,10 @@ pub async fn sync_provider_models_once(state: &super::JobState) {
Ok(summary) => {
tracing::info!(
"[JOBS] Synced provider '{}': +{}/~{}/-{} models",
- provider.name, summary.models_added, summary.models_updated, summary.models_removed
+ provider.name,
+ summary.models_added,
+ summary.models_updated,
+ summary.models_removed
);
}
Err(e) => {
diff --git a/src/main.rs b/src/main.rs
index 83f9e84..5ecdfcc 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,5 @@
+extern crate core;
+
mod ai;
mod config;
mod i18n;
@@ -17,8 +19,7 @@ use std::sync::Arc;
async fn main() {
dotenv::dotenv().ok();
- let log_filter = tracing_subscriber::EnvFilter::try_from_default_env()
- .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,omniference=debug"));
+ let log_filter = tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,omniference=debug"));
tracing_subscriber::fmt().with_env_filter(log_filter).with_target(true).init();
let db_host = std::env::var("POSTGRES_HOST").unwrap_or_else(|_| "localhost".to_string());
@@ -55,19 +56,19 @@ async fn main() {
}
}
+ match utils::encryption::init() {
+ Ok(source) => println!("[ENCRYPTION] Credential encryption enabled using {}", source.as_str()),
+ Err(error) => {
+ eprintln!("[ENCRYPTION] Failed to initialize credential encryption: {error}");
+ std::process::exit(1);
+ }
+ }
config::Config::init(&pool).await;
println!("[CONFIG] Configuration loaded");
i18n::I18n::init(&pool).await;
println!("[I18N] Translations loaded");
- utils::encryption::init();
- if utils::encryption::is_enabled() {
- println!("[ENCRYPTION] API key encryption enabled");
- } else {
- println!("[ENCRYPTION] API key encryption disabled (set ENCRYPTION_KEY to enable)");
- }
-
ai::init(&pool).await;
let app_state = Arc::new(JobState {
diff --git a/src/routes/admin/config.rs b/src/routes/admin/config.rs
index 868e704..d908bbb 100644
--- a/src/routes/admin/config.rs
+++ b/src/routes/admin/config.rs
@@ -23,7 +23,7 @@ pub async fn get_global_config() -> impl IntoResponse {
default_theme: config.default_theme(),
enable_provider_selector: config.enable_provider_selector(),
allow_server_stdio_mcp: config.allow_server_stdio_mcp(),
- default_model_key: config.default_model_key(),
+ default_model_id: config.default_model_id(),
};
ResponseBuilder::new(ResponseBody::Json(response)).build()
}
@@ -62,9 +62,9 @@ pub async fn update_global_config(State(state): State>, cookies: C
}
}
- if let Some(default_model_key) = req.default_model_key {
- if let Err(e) = Config::get().set_default_model_key(&state.db, default_model_key.as_deref()).await {
- eprintln!("[CONFIG] Failed to update default_model_key: {e}");
+ if let Some(default_model_id) = req.default_model_id {
+ if let Err(e) = Config::get().set_default_model_id(&state.db, default_model_id).await {
+ eprintln!("[CONFIG] Failed to update default_model_id: {e}");
return ErrorBuilder::new(ErrorCode::InternalError).build();
}
}
@@ -74,7 +74,7 @@ pub async fn update_global_config(State(state): State>, cookies: C
default_theme: config.default_theme(),
enable_provider_selector: config.enable_provider_selector(),
allow_server_stdio_mcp: config.allow_server_stdio_mcp(),
- default_model_key: config.default_model_key(),
+ default_model_id: config.default_model_id(),
};
ResponseBuilder::new(ResponseBody::Json(response)).build()
}
diff --git a/src/routes/admin/mod.rs b/src/routes/admin/mod.rs
index 6a2d702..d8c93f7 100644
--- a/src/routes/admin/mod.rs
+++ b/src/routes/admin/mod.rs
@@ -5,6 +5,7 @@ pub mod config;
pub mod i18n;
pub mod mcp;
pub mod models;
+pub mod provider_billing;
pub mod providers;
pub mod teams;
pub mod tools;
diff --git a/src/routes/admin/provider_billing.rs b/src/routes/admin/provider_billing.rs
new file mode 100644
index 0000000..42432cf
--- /dev/null
+++ b/src/routes/admin/provider_billing.rs
@@ -0,0 +1,126 @@
+use super::providers::{ADMIN_PROVIDERS_EDIT, ADMIN_PROVIDERS_VIEW};
+use crate::routes::public::auth::get_current_user;
+use crate::types::JobState;
+use crate::types::providers::{Provider, ProviderKind, UpdateProviderBillingRequest};
+use crate::utils::encryption::{encrypt_api_key, is_enabled};
+use crate::utils::provider_billing::refresh_provider_billing;
+use crate::utils::response::{ErrorBuilder, ErrorCode, ResponseBody, ResponseBuilder};
+use axum::{
+ Json,
+ extract::{Path, State},
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+use std::sync::Arc;
+use tower_cookies::Cookies;
+use uuid::Uuid;
+
+pub async fn list_billing(State(state): State>, cookies: Cookies) -> Response {
+ if let Err(response) = authorize(&state, &cookies, ADMIN_PROVIDERS_VIEW).await {
+ return response;
+ }
+ match Provider::list_billing_overviews_for_admin(&state.db).await {
+ Ok(overviews) => ResponseBuilder::new(ResponseBody::Json(overviews)).build(),
+ Err(error) => {
+ tracing::error!("Failed to list provider billing overviews: {error}");
+ ErrorBuilder::new(ErrorCode::InternalError).build()
+ }
+ }
+}
+
+pub async fn get_billing(State(state): State>, cookies: Cookies, Path(id): Path) -> Response {
+ if let Err(response) = authorize(&state, &cookies, ADMIN_PROVIDERS_VIEW).await {
+ return response;
+ }
+ overview_response(&state, &id).await
+}
+
+pub async fn update_billing(State(state): State>, cookies: Cookies, Path(id): Path, Json(request): Json) -> Response {
+ if let Err(response) = authorize(&state, &cookies, ADMIN_PROVIDERS_EDIT).await {
+ return response;
+ }
+ let provider = match Provider::find_for_admin(&state.db, &id).await {
+ Ok(Some(provider)) => provider,
+ Ok(None) => return ErrorBuilder::new(ErrorCode::NotFound).build(),
+ Err(_) => return ErrorBuilder::new(ErrorCode::InternalError).build(),
+ };
+ if request.credential.as_ref().is_some_and(|value| value.trim().is_empty()) {
+ return ErrorBuilder::new(ErrorCode::ValidationFailed).field("credential").build();
+ }
+ if request.credential.is_some() && !is_enabled() {
+ return ErrorBuilder::new(ErrorCode::BadRequest)
+ .details(serde_json::json!({"reason": "encryption_required"}))
+ .build();
+ }
+ if matches!(
+ provider.kind,
+ ProviderKind::Openai | ProviderKind::Google | ProviderKind::OpenaiCompat | ProviderKind::Custom | ProviderKind::Anthropic
+ ) {
+ return ErrorBuilder::new(ErrorCode::BadRequest).details(serde_json::json!({"reason": "unsupported"})).build();
+ }
+ let encrypted = match request.credential.as_deref().map(encrypt_api_key).transpose() {
+ Ok(credential) => credential,
+ Err(error) => {
+ tracing::error!(provider_id=%id, "Failed to encrypt billing credential: {error}");
+ return ErrorBuilder::new(ErrorCode::InternalError).build();
+ }
+ };
+ match Provider::upsert_billing_connection_for_admin(&state.db, &id, encrypted.as_deref()).await {
+ Ok(Some(_)) => overview_response(&state, &id).await,
+ Ok(None) => ErrorBuilder::new(ErrorCode::NotFound).build(),
+ Err(error) => {
+ tracing::error!(provider_id=%id, "Failed to save provider billing configuration: {error}");
+ ErrorBuilder::new(ErrorCode::InternalError).build()
+ }
+ }
+}
+
+pub async fn delete_billing(State(state): State>, cookies: Cookies, Path(id): Path) -> Response {
+ if let Err(response) = authorize(&state, &cookies, ADMIN_PROVIDERS_EDIT).await {
+ return response;
+ }
+ match Provider::delete_billing_connection_for_admin(&state.db, &id).await {
+ Ok(true) => (StatusCode::NO_CONTENT, "").into_response(),
+ Ok(false) => ErrorBuilder::new(ErrorCode::NotFound).build(),
+ Err(error) => {
+ tracing::error!(provider_id=%id, "Failed to remove provider billing configuration: {error}");
+ ErrorBuilder::new(ErrorCode::InternalError).build()
+ }
+ }
+}
+
+pub async fn refresh_billing(State(state): State>, cookies: Cookies, Path(id): Path) -> Response {
+ if let Err(response) = authorize(&state, &cookies, ADMIN_PROVIDERS_EDIT).await {
+ return response;
+ }
+ let provider = match Provider::find_for_admin(&state.db, &id).await {
+ Ok(Some(provider)) => provider,
+ Ok(None) => return ErrorBuilder::new(ErrorCode::NotFound).build(),
+ Err(_) => return ErrorBuilder::new(ErrorCode::InternalError).build(),
+ };
+ if let Err(error) = refresh_provider_billing(&state.db, &provider).await {
+ tracing::warn!(provider_id=%provider.id, provider_name=%provider.name, category=error.code(), "Provider billing refresh failed");
+ }
+ overview_response(&state, &id).await
+}
+
+async fn authorize(state: &JobState, cookies: &Cookies, permission: &str) -> Result<(), Response> {
+ let Some(user) = get_current_user(&state.db, cookies).await else {
+ return Err(ErrorBuilder::new(ErrorCode::NotAuthenticated).build());
+ };
+ if !user.has_permission(&state.db, permission).await {
+ return Err(ErrorBuilder::new(ErrorCode::InsufficientPermissions).build());
+ }
+ Ok(())
+}
+
+async fn overview_response(state: &JobState, id: &Uuid) -> Response {
+ match Provider::billing_overview_for_admin(&state.db, id).await {
+ Ok(Some(overview)) => ResponseBuilder::new(ResponseBody::Json(overview)).build(),
+ Ok(None) => ErrorBuilder::new(ErrorCode::NotFound).build(),
+ Err(error) => {
+ tracing::error!(provider_id=%id, "Failed to load provider billing overview: {error}");
+ ErrorBuilder::new(ErrorCode::InternalError).build()
+ }
+ }
+}
diff --git a/src/routes/admin/providers.rs b/src/routes/admin/providers.rs
index 28a288e..3c7548b 100644
--- a/src/routes/admin/providers.rs
+++ b/src/routes/admin/providers.rs
@@ -92,7 +92,13 @@ pub async fn create_provider(State(state): State>, cookies: Cookie
return ErrorBuilder::new(ErrorCode::InsufficientPermissions).build();
}
- let api_key = req.api_key.as_ref().map(|k| encrypt_api_key(k));
+ let api_key = match req.api_key.as_deref().map(encrypt_api_key).transpose() {
+ Ok(api_key) => api_key,
+ Err(error) => {
+ tracing::error!("Failed to encrypt provider credential: {error}");
+ return ErrorBuilder::new(ErrorCode::InternalError).build();
+ }
+ };
let base_url = req.base_url.trim_end_matches('/').to_string();
let provider = Provider::create_system(&state.db, &req.kind, &req.name, &base_url, api_key.as_deref(), &req.extra_headers, req.is_enabled).await;
@@ -128,7 +134,13 @@ pub async fn update_provider(State(state): State>, cookies: Cookie
}
let base_url = req.base_url.as_deref().map(|url| url.trim_end_matches('/').to_string());
- let api_key = req.api_key.as_ref().map(|key| encrypt_api_key(key));
+ let api_key = match req.api_key.as_deref().map(encrypt_api_key).transpose() {
+ Ok(api_key) => api_key,
+ Err(error) => {
+ tracing::error!("Failed to encrypt provider credential: {error}");
+ return ErrorBuilder::new(ErrorCode::InternalError).build();
+ }
+ };
let provider = Provider::patch_system(
&state.db,
&id,
@@ -204,8 +216,20 @@ pub async fn test_provider(State(state): State>, cookies: Cookies,
}
};
- let api_key = provider.api_key.as_ref().map(|k| decrypt_api_key(k));
- let extra_headers = parse_extra_headers(&provider.extra_headers.0);
+ let api_key = match provider.api_key.as_deref().map(decrypt_api_key).transpose() {
+ Ok(api_key) => api_key,
+ Err(error) => {
+ tracing::error!(provider_id=%provider.id, "Failed to decrypt provider credential: {error}");
+ return ErrorBuilder::new(ErrorCode::InternalError).build();
+ }
+ };
+ let extra_headers = match parse_extra_headers(&provider.extra_headers.0) {
+ Ok(headers) => headers,
+ Err(error) => {
+ tracing::error!(provider_id=%provider.id, "Failed to decrypt provider headers: {error}");
+ return ErrorBuilder::new(ErrorCode::InternalError).build();
+ }
+ };
let config = ProviderConfig {
name: provider.name.clone(),
@@ -245,7 +269,13 @@ pub async fn test_provider_inline(State(state): State>, cookies: C
return ErrorBuilder::new(ErrorCode::InsufficientPermissions).build();
}
- let extra_headers = parse_extra_headers(&req.extra_headers);
+ let extra_headers = match parse_extra_headers(&req.extra_headers) {
+ Ok(headers) => headers,
+ Err(error) => {
+ tracing::error!("Failed to read provider headers: {error}");
+ return ErrorBuilder::new(ErrorCode::InternalError).build();
+ }
+ };
let config = ProviderConfig {
name: "test".to_string(),
diff --git a/src/routes/admin/tools.rs b/src/routes/admin/tools.rs
index 1379c9d..1b52a2e 100644
--- a/src/routes/admin/tools.rs
+++ b/src/routes/admin/tools.rs
@@ -70,14 +70,13 @@ pub async fn list_tools(State(state): State>, cookies: Cookies) ->
.collect();
if !server_ids.is_empty() {
- let server_names: HashMap =
- sqlx::query_as::<_, (Uuid, String)>("SELECT id, name FROM mcp_servers WHERE id = ANY($1)")
- .bind(&server_ids)
- .fetch_all(&state.db)
- .await
- .unwrap_or_default()
- .into_iter()
- .collect();
+ let server_names: HashMap = sqlx::query_as::<_, (Uuid, String)>("SELECT id, name FROM mcp_servers WHERE id = ANY($1)")
+ .bind(&server_ids)
+ .fetch_all(&state.db)
+ .await
+ .unwrap_or_default()
+ .into_iter()
+ .collect();
for r in &mut responses {
r.mcp_server_name = r.mcp_server_id.and_then(|id| server_names.get(&id).cloned());
@@ -200,14 +199,9 @@ pub async fn get_tool_settings(State(state): State>, cookies: Cook
return ErrorBuilder::new(ErrorCode::InsufficientPermissions).build();
}
- let settings = sqlx::query_as::<_, UserToolSettings>("SELECT * FROM user_tool_settings WHERE tool_id = $1 AND user_id IS NULL")
- .bind(tool_id)
- .fetch_optional(&state.db)
- .await;
-
- match settings {
- Ok(Some(s)) => ResponseBuilder::new(ResponseBody::Json(s.settings)).build(),
- Ok(None) => ResponseBuilder::new(ResponseBody::Json(serde_json::json!({}))).build(),
+ match Tool::system_settings_for_admin(&state.db, &tool_id).await {
+ Ok(settings) => ResponseBuilder::new(ResponseBody::Json(settings)).build(),
+ Err(ToolSettingsError::NotFound) => ErrorBuilder::new(ErrorCode::NotFound).build(),
Err(e) => {
eprintln!("[TOOLS] Failed to get tool settings: {e}");
ErrorBuilder::new(ErrorCode::InternalError).build()
@@ -239,6 +233,10 @@ pub async fn set_tool_settings(
eprintln!("[TOOLS] Failed to save tool settings: {e}");
ErrorBuilder::new(ErrorCode::InternalError).build()
}
+ Err(ToolSettingsError::Encryption(e)) => {
+ eprintln!("[TOOLS] Failed to protect tool settings: {e}");
+ ErrorBuilder::new(ErrorCode::InternalError).build()
+ }
}
}
@@ -350,16 +348,17 @@ pub async fn test_tool(State(state): State>, cookies: Cookies, Pat
}
};
- let tool_settings = sqlx::query_as::<_, UserToolSettings>("SELECT * FROM user_tool_settings WHERE tool_id = $1 AND user_id IS NULL")
- .bind(tool_id)
- .fetch_optional(&state.db)
- .await
- .ok()
- .flatten();
+ let tool_settings = match Tool::effective_settings_for_user(&state.db, &tool_id, None).await {
+ Ok(settings) => settings,
+ Err(error) => {
+ eprintln!("[TOOLS] Failed to load tool settings: {error}");
+ return ErrorBuilder::new(ErrorCode::InternalError).build();
+ }
+ };
let ctx = crate::utils::tools::ToolContext {
user_id: None,
- settings: tool_settings.map(|s| s.settings).unwrap_or_default(),
+ settings: tool_settings,
timeout_ms: Some(30000),
function_name: req.function_name.clone(),
db: Some(std::sync::Arc::new(state.db.clone())),
@@ -407,7 +406,14 @@ pub async fn test_tool(State(state): State>, cookies: Cookies, Pat
}
}
ToolSourceKind::Http => {
- let config: HttpSourceConfig = match serde_json::from_value(tool.source_config.clone()) {
+ let source_config = match tool.revealed_source_config() {
+ Ok(config) => config,
+ Err(e) => {
+ eprintln!("[TOOLS] Failed to decrypt HTTP tool credentials: {e}");
+ return ErrorBuilder::new(ErrorCode::InternalError).build();
+ }
+ };
+ let config: HttpSourceConfig = match serde_json::from_value(source_config) {
Ok(c) => c,
Err(e) => {
eprintln!("[TOOLS] Invalid HTTP config: {e}");
diff --git a/src/routes/mod.rs b/src/routes/mod.rs
index 6dbf966..dc41621 100644
--- a/src/routes/mod.rs
+++ b/src/routes/mod.rs
@@ -22,6 +22,14 @@ pub fn build_router() -> Router> {
// Admin AI Providers
.route("/api/v1/admin/providers", get(admin::providers::list_providers))
.route("/api/v1/admin/providers", post(admin::providers::create_provider))
+ .route("/api/v1/admin/providers/billing", get(admin::provider_billing::list_billing))
+ .route(
+ "/api/v1/admin/providers/{id}/billing",
+ get(admin::provider_billing::get_billing)
+ .put(admin::provider_billing::update_billing)
+ .delete(admin::provider_billing::delete_billing),
+ )
+ .route("/api/v1/admin/providers/{id}/billing/refresh", post(admin::provider_billing::refresh_billing))
.route("/api/v1/admin/providers/test", post(admin::providers::test_provider_inline))
.route("/api/v1/admin/providers/{id}", get(admin::providers::get_provider))
.route("/api/v1/admin/providers/{id}", put(admin::providers::update_provider))
diff --git a/src/routes/public/base.rs b/src/routes/public/base.rs
index 0d0eda4..8b9e147 100644
--- a/src/routes/public/base.rs
+++ b/src/routes/public/base.rs
@@ -50,7 +50,7 @@ pub async fn get_base(State(state): State>) -> impl IntoResponse {
roles,
enable_provider_selector: crate::config::Config::get().enable_provider_selector(),
allow_server_stdio_mcp: crate::config::Config::get().allow_server_stdio_mcp(),
- default_model_key: crate::config::Config::get().default_model_key(),
+ default_model_id: crate::config::Config::get().default_model_id(),
}))
.build()
}
diff --git a/src/routes/public/streaming.rs b/src/routes/public/streaming.rs
index d27ba5d..b25eba6 100644
--- a/src/routes/public/streaming.rs
+++ b/src/routes/public/streaming.rs
@@ -8,7 +8,7 @@ use crate::types::models::{Model, ModelPricing};
use crate::types::models_configs::{ModelConfig, ModelConfigViewer};
use crate::types::{
Budget, Chat, ChatMessageResponse, ClientToolResultRequest, Message, MessagePart, RequestSettings, StreamData, StreamRequest, StreamingAssistantMessageCreate,
- StreamingUserMessageCreate, Tool, ToolExecution, ToolExecutionInternal, ToolExecutionResponse, ToolFunction, UsageEvent, UsageEventRecord, UserToolSettings,
+ StreamingUserMessageCreate, Tool, ToolExecution, ToolExecutionInternal, ToolExecutionResponse, ToolFunction, UsageEvent, UsageEventRecord,
};
use crate::types::{CostDetails, JobState};
use crate::utils::tools::{HttpExecutor, ToolContext, ToolExecutor, get_builtin_executor};
@@ -27,7 +27,12 @@ use omniference::{
types::{ChatRequestIR, ContentPart, Message as OmniMessage, Role, ToolChoice, ToolSpec},
};
use rust_decimal::{Decimal, prelude::FromPrimitive};
-use std::{collections::{HashMap, HashSet}, convert::Infallible, sync::Arc, time::Instant};
+use std::{
+ collections::{HashMap, HashSet},
+ convert::Infallible,
+ sync::Arc,
+ time::Instant,
+};
use tower_cookies::Cookies;
use uuid::Uuid;
@@ -168,7 +173,11 @@ async fn resolve_tools(db: &sqlx::PgPool, user_id: Uuid, enabled_tool_ids: &[Str
let mut system_prompts: Vec = vec![];
for tool in tools {
if let Some(prompt) = tool.system_prompt.as_ref().map(|p| p.trim()).filter(|p| !p.is_empty()) {
- let label = if tool.display_name.trim().is_empty() { tool.name.as_str() } else { tool.display_name.trim() };
+ let label = if tool.display_name.trim().is_empty() {
+ tool.name.as_str()
+ } else {
+ tool.display_name.trim()
+ };
system_prompts.push(format!("Instructions for the \"{label}\" tool:\n{prompt}"));
}
let funcs = functions_by_tool.get(&tool.id).map(|v| v.as_slice()).unwrap_or(&[]);
@@ -246,12 +255,9 @@ async fn execute_tool_by_name(
None
};
- let settings = UserToolSettings::find_effective_for_user(db, &tool.id, &user_id)
+ let settings = Tool::effective_settings_for_user(db, &tool.id, Some(&user_id))
.await
- .ok()
- .flatten()
- .map(|s| s.settings)
- .unwrap_or_else(|| serde_json::json!({}));
+ .map_err(|error| format!("Failed to load tool settings: {error}"))?;
let ctx = ToolContext {
user_id: Some(user_id),
@@ -278,7 +284,10 @@ async fn execute_tool_by_name(
})
}
ToolSourceKind::Http => {
- let base_url = tool.source_config.get("url").and_then(|v| v.as_str()).unwrap_or("");
+ let source_config = tool
+ .revealed_source_config()
+ .map_err(|error| format!("Failed to decrypt HTTP tool credentials: {error}"))?;
+ let base_url = source_config.get("url").and_then(|v| v.as_str()).unwrap_or("");
let url = if let Some(entrypoint) = &function_entrypoint {
if entrypoint.starts_with("http") {
entrypoint.clone()
@@ -289,17 +298,13 @@ async fn execute_tool_by_name(
base_url.to_string()
};
- let headers: HashMap = tool
- .source_config
- .get("headers")
- .and_then(|v| serde_json::from_value(v.clone()).ok())
- .unwrap_or_default();
+ let headers: HashMap = source_config.get("headers").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
let http_config = crate::utils::tools::http::HttpConfig {
- method: tool.source_config.get("method").and_then(|v| v.as_str()).unwrap_or("GET").to_string(),
+ method: source_config.get("method").and_then(|v| v.as_str()).unwrap_or("GET").to_string(),
url,
headers,
- body_template: tool.source_config.get("body_template").and_then(|v| v.as_str()).map(String::from),
+ body_template: source_config.get("body_template").and_then(|v| v.as_str()).map(String::from),
};
let executor = HttpExecutor::new(full_tool_name.to_string(), http_config).map_err(|e| format!("{:?}", e))?;
@@ -382,7 +387,9 @@ async fn hydrate_image(db: &sqlx::PgPool, image_id: &str, vision: bool, generate
"[An image was generated earlier (image_id: {image_id}) from the prompt: \"{}\". It is already displayed to the user. To modify it, call imagegen_edit with image_id \"{image_id}\".]",
c.trim()
),
- _ => format!("[An image was generated earlier (image_id: {image_id}) and is already displayed to the user. To modify it, call imagegen_edit with image_id \"{image_id}\".]"),
+ _ => format!(
+ "[An image was generated earlier (image_id: {image_id}) and is already displayed to the user. To modify it, call imagegen_edit with image_id \"{image_id}\".]"
+ ),
};
parts.push(ContentPart::Text(text));
} else {
@@ -469,7 +476,11 @@ async fn get_omni_messages(db: &sqlx::PgPool, messages: Vec, vision: bo
if parts.is_empty() {
parts.push(ContentPart::Text(m.content));
}
- result.push(OmniMessage { role: Role::User, parts, name: None });
+ result.push(OmniMessage {
+ role: Role::User,
+ parts,
+ name: None,
+ });
continue;
}
@@ -493,7 +504,11 @@ async fn get_omni_messages(db: &sqlx::PgPool, messages: Vec, vision: bo
}
MessagePart::ToolResult { tool_call_id } => {
if !buffer.is_empty() {
- result.push(OmniMessage { role: Role::Assistant, parts: std::mem::take(&mut buffer), name: None });
+ result.push(OmniMessage {
+ role: Role::Assistant,
+ parts: std::mem::take(&mut buffer),
+ name: None,
+ });
}
unresolved_calls.retain(|c| c != &tool_call_id);
result.push(tool_message(execs, &call_names, &tool_call_id));
@@ -502,7 +517,11 @@ async fn get_omni_messages(db: &sqlx::PgPool, messages: Vec, vision: bo
}
if !buffer.is_empty() {
- result.push(OmniMessage { role: Role::Assistant, parts: buffer, name: None });
+ result.push(OmniMessage {
+ role: Role::Assistant,
+ parts: buffer,
+ name: None,
+ });
}
// Legacy messages (saved before tool-result markers) and any call whose result marker
// is missing: emit their `tool` messages here so no assistant tool_call is left dangling.
diff --git a/src/tests/encryption.rs b/src/tests/encryption.rs
new file mode 100644
index 0000000..8480896
--- /dev/null
+++ b/src/tests/encryption.rs
@@ -0,0 +1,66 @@
+#[cfg(test)]
+mod credential_encryption {
+ use crate::utils::encryption::{
+ EncryptionError, SECRET_MASK, decrypt_api_key, decrypt_object_values, decrypt_schema_secrets, encrypt_api_key, encrypt_object_values, encrypt_schema_secrets,
+ init_for_test, mask_object_values, mask_schema_secrets,
+ };
+ use aes_gcm::{
+ Aes256Gcm, Nonce,
+ aead::{Aead, KeyInit},
+ };
+ use std::fmt::Write;
+
+ #[test]
+ fn encrypts_and_authenticates_credentials() -> Result<(), EncryptionError> {
+ init_for_test([0x42; 32])?;
+ let encrypted = encrypt_api_key("provider-secret")?;
+ assert!(encrypted.starts_with("oxide:v1:"));
+ assert_eq!(decrypt_api_key(&encrypted)?, "provider-secret");
+ assert_eq!(decrypt_api_key("existing-plaintext")?, "existing-plaintext");
+ let nonce = [0x24; 12];
+ let cipher = Aes256Gcm::new([0x42; 32].as_ref().into());
+ let mut legacy = cipher
+ .encrypt(Nonce::from_slice(&nonce), b"legacy-secret".as_ref())
+ .map_err(|_| EncryptionError::EncryptionFailed)?;
+ legacy.extend_from_slice(&nonce);
+ let legacy = legacy.iter().fold(String::from("enc:"), |mut encoded, byte| {
+ let _ = write!(encoded, "{byte:02x}");
+ encoded
+ });
+ assert_eq!(decrypt_api_key(&legacy)?, "legacy-secret");
+
+ let mut tampered = encrypted.into_bytes();
+ let last = tampered.len() - 1;
+ tampered[last] = if tampered[last] == b'A' { b'B' } else { b'A' };
+ let tampered = String::from_utf8(tampered).map_err(|_| EncryptionError::InvalidFormat)?;
+ assert!(decrypt_api_key(&tampered).is_err());
+ Ok(())
+ }
+
+ #[test]
+ fn protects_structured_secrets_and_preserves_masks() -> Result<(), EncryptionError> {
+ init_for_test([0x42; 32])?;
+ let schema = serde_json::json!({
+ "type": "object",
+ "properties": {
+ "api_key": {"type": "string", "secret": true},
+ "provider": {"type": "string"}
+ }
+ });
+ let settings = serde_json::json!({"api_key": "secret", "provider": "tavily"});
+ let protected = encrypt_schema_secrets(&settings, &schema, None)?;
+ assert!(protected["api_key"].as_str().is_some_and(|value| value.starts_with("oxide:v1:")));
+ assert_eq!(protected["provider"], "tavily");
+ assert_eq!(decrypt_schema_secrets(&protected, &schema)?, settings);
+ assert_eq!(mask_schema_secrets(&protected, &schema)?["api_key"], SECRET_MASK);
+ let update = serde_json::json!({"api_key": SECRET_MASK, "provider": "exa"});
+ let updated = encrypt_schema_secrets(&update, &schema, Some(&protected))?;
+ assert_eq!(updated["api_key"], protected["api_key"]);
+
+ let headers = serde_json::json!({"Authorization": "Bearer secret"});
+ let protected_headers = encrypt_object_values(&headers, None)?;
+ assert_eq!(decrypt_object_values(&protected_headers)?, headers);
+ assert_eq!(mask_object_values(&protected_headers)["Authorization"], SECRET_MASK);
+ Ok(())
+ }
+}
diff --git a/src/tests/mcp.rs b/src/tests/mcp.rs
index 481f602..7a0201f 100644
--- a/src/tests/mcp.rs
+++ b/src/tests/mcp.rs
@@ -184,16 +184,16 @@ mod tests {
}
#[sqlx::test(migrations = "./migrations")]
- async fn mcp_server_response_masks_secrets_when_requested(pool: PgPool) {
- let server = McpServer::create(&pool, None, "sys_secret", "http", &http_config_with_secret("https://sys.example"), true)
- .await
- .unwrap();
+ async fn mcp_server_response_masks_secrets_when_requested(pool: PgPool) -> Result<(), Box> {
+ crate::utils::encryption::init_for_test([0x42; 32])?;
+ let server = McpServer::create(&pool, None, "sys_secret", "http", &http_config_with_secret("https://sys.example"), true).await?;
let masked = McpServerResponse::from_server(server.clone(), false);
assert_eq!(masked.connection_config["headers"]["Authorization"], "***");
let unmasked = McpServerResponse::from_server(server, true);
assert_eq!(unmasked.connection_config["headers"]["Authorization"], "Bearer secret");
+ Ok(())
}
#[tokio::test]
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index b41be28..003ee6b 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -2,6 +2,7 @@ pub mod auth;
pub mod base;
pub mod catalog;
pub mod config;
+pub mod encryption;
pub mod i18n;
pub mod images;
pub mod logging;
diff --git a/src/types/AGENTS.md b/src/types/AGENTS.md
index b4f8a14..cfae525 100644
--- a/src/types/AGENTS.md
+++ b/src/types/AGENTS.md
@@ -22,9 +22,9 @@ src/types//
Only add more files when the domain has a real boundary that this layout does not cover.
-## `mod.rs`
+## `providers_billing`
-`mod.rs` should contain the main database row struct and domain-local context structs.
+`providers_billing` should contain the main database row struct and domain-local context structs.
Example shape:
@@ -45,7 +45,7 @@ pub struct DomainViewer<'a> {
impl BaseType for Domain {}
```
-Keep call-site imports stable by re-exporting public request and response DTOs from `mod.rs`.
+Keep call-site imports stable by re-exporting public request and response DTOs from `providers_billing`.
Do not re-export private SQL row structs.
@@ -196,7 +196,7 @@ When porting another type to this pattern:
1. Move public request DTOs into `requests.rs`.
2. Move public response DTOs into `responses.rs`.
-3. Keep the database row type in `mod.rs`.
+3. Keep the database row type in `providers_billing`.
4. Add private checked row structs in `rows.rs`.
5. Move SQL methods into `repository.rs` and `patch.rs`.
6. Replace dynamic projections with static `query_as!` queries.
diff --git a/src/types/base/responses.rs b/src/types/base/responses.rs
index 3a5341a..ef1a024 100644
--- a/src/types/base/responses.rs
+++ b/src/types/base/responses.rs
@@ -3,6 +3,7 @@ use crate::types::Role;
use serde::Serialize;
use serde_json::Value;
use std::sync::Arc;
+use uuid::Uuid;
#[derive(Debug, Serialize)]
pub struct BaseResponse {
@@ -15,6 +16,6 @@ pub struct BaseResponse {
pub enable_provider_selector: bool,
/// Whether admins may register server-side stdio MCP servers.
pub allow_server_stdio_mcp: bool,
- /// Instance-wide default model key.
- pub default_model_key: Option,
+ /// Instance-wide default model ID.
+ pub default_model_id: Option,
}
diff --git a/src/types/chat/requests.rs b/src/types/chat/requests.rs
index 7381a7a..bb53073 100644
--- a/src/types/chat/requests.rs
+++ b/src/types/chat/requests.rs
@@ -122,7 +122,7 @@ pub struct UpdateGlobalConfigRequest {
pub enable_provider_selector: Option,
pub allow_server_stdio_mcp: Option,
#[serde(default, deserialize_with = "deserialize_nullable_field")]
- pub default_model_key: Option