mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-18 10:12:55 +02:00
Compare commits
254 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fd990df93 | |||
| 1f390cabc6 | |||
| 7f1f8c54c1 | |||
| b1515db229 | |||
| d78c226734 | |||
| 52865bc1f5 | |||
| efbb6c9814 | |||
| 1e3dea3dad | |||
| 25f84482ba | |||
| 3f2a116fdb | |||
| dce002c6cc | |||
| dcfbbdc79d | |||
| f8cac37eef | |||
| cb37713c4c | |||
| 7ae4082d7e | |||
| aa8d339d35 | |||
| 7a61b182e0 | |||
| 91b697b80b | |||
| 7d10b63dc5 | |||
| 92916cd9db | |||
| 075d112861 | |||
| 7e9407fbc7 | |||
| 313f777e44 | |||
| 3ba9eb7ad7 | |||
| 328a2d52da | |||
| 697bea814e | |||
| 83cda218b7 | |||
| 76435440af | |||
| 0a8b5e46ca | |||
| a74b0934fc | |||
| c05d2aeafb | |||
| 87a3e5ceea | |||
| 555b36e13e | |||
| 021866011f | |||
| 749abdcb56 | |||
| 7d6f3a2925 | |||
| af515f1072 | |||
| c90c67eab3 | |||
| 51c9507d08 | |||
| 11daf4f927 | |||
| 58195b5959 | |||
| b51aecf45a | |||
| ca7ea72ff1 | |||
| 53b1d8a9f3 | |||
| a3dfb30a42 | |||
| 111d4b1b8c | |||
| 32d76ee2ac | |||
| 98ad6fb31b | |||
| 3a18a3994f | |||
| 6d35b20880 | |||
| 173d4a536f | |||
| d7bff686c0 | |||
| 193f6da8d1 | |||
| 954dc8d1d9 | |||
| 234819f8f2 | |||
| b34a8d116b | |||
| 7375b9efae | |||
| c8f170d197 | |||
| 4989d1e31a | |||
| c568251d8d | |||
| db2de4af89 | |||
| 574ae8f5f9 | |||
| 125091d719 | |||
| 5d37f44241 | |||
| f0f5e9219b | |||
| ee75a09f8c | |||
| c49c6f5837 | |||
| 03118f1ede | |||
| 0ff5ea4f6a | |||
| 978fa8bfc6 | |||
| 36c7c5dc34 | |||
| 8f3628d4ec | |||
| fcd3c72cac | |||
| 3f0be4a39d | |||
| f9ff3a5715 | |||
| ed71c90d73 | |||
| e580c7db7c | |||
| 4a9b0117a2 | |||
| 9b10292e02 | |||
| d9a9ae14fd | |||
| db716f781e | |||
| e92bb9043b | |||
| a2f5d830d6 | |||
| 77e3422e88 | |||
| 9a551c91e5 | |||
| c35ac493ed | |||
| e100645a00 | |||
| 96bfc66a94 | |||
| e21c9eec31 | |||
| a9f32102b5 | |||
| 7291628f86 | |||
| 5fbb205044 | |||
| 02ab24d01e | |||
| 3cfbd66a80 | |||
| 860fa47b7c | |||
| 71d9d8f010 | |||
| cc58e28e54 | |||
| 653dd920ad | |||
| dd78c2cc08 | |||
| 47ae017f94 | |||
| c33fa8782b | |||
| 93cba1d098 | |||
| d3a1f9798d | |||
| 115c3cbf07 | |||
| bed5817da3 | |||
| 83035753af | |||
| 379b53bcca | |||
| 355294a2d4 | |||
| 833b27ab4a | |||
| 8a04f13dd4 | |||
| 49020fa6ea | |||
| ac3402a8b3 | |||
| 9369e60695 | |||
| 295e38d2af | |||
| 11595f5f96 | |||
| b08e875b37 | |||
| 0016b8fce7 | |||
| f5b10df7cf | |||
| 1782f31075 | |||
| f7aca85417 | |||
| c42df737b0 | |||
| 75c6af3b11 | |||
| e734cc93a1 | |||
| e3ccc4f8d4 | |||
| 896c362dce | |||
| 864ab97293 | |||
| bc2f291352 | |||
| 98f5e39a7f | |||
| e0a66e79bb | |||
| c781be4e70 | |||
| 48c4b44f72 | |||
| 68a84181d9 | |||
| b60390d805 | |||
| b33e776072 | |||
| 2187f23588 | |||
| 732fd4eba1 | |||
| 495c772d6b | |||
| 02e4fe8b48 | |||
| 6d45486bb5 | |||
| 4c1f7f6243 | |||
| 1e3a9fb921 | |||
| 8b8e7913dc | |||
| 88515a7dad | |||
| e012063f5b | |||
| e76377d3c7 | |||
| a357f42c48 | |||
| 2becb15916 | |||
| 186eeed784 | |||
| 216c932cb9 | |||
| 52de5eb539 | |||
| eec3119297 | |||
| f2e7f83b3d | |||
| 93dea0525e | |||
| 7e6e0a8a9c | |||
| 211d3a53f5 | |||
| 187936e5dd | |||
| b180d064d1 | |||
| ed46afd81a | |||
| 0050fad70d | |||
| 3dc9c100e9 | |||
| 969e7c9296 | |||
| 591caab45a | |||
| 02e97d6ae4 | |||
| 996160fadc | |||
| dc1923ab1f | |||
| fe5831eb48 | |||
| ca79abc9d4 | |||
| 9eafa067b9 | |||
| 5e5e172d39 | |||
| bc7a883f6c | |||
| 12056aebc6 | |||
| 297cb9c8f2 | |||
| d699455b38 | |||
| 9e7b4afaec | |||
| e91c344e64 | |||
| 07f628b928 | |||
| 184e1425a4 | |||
| 22f2990f56 | |||
| 6c28c5f383 | |||
| e9f9cf54f4 | |||
| 751d1b083d | |||
| a4d77a4fd3 | |||
| f72252552f | |||
| c5d68675c9 | |||
| 36b8ef5fba | |||
| 6cca5e0472 | |||
| b4d2974e19 | |||
| fb896d6c0f | |||
| 6564bfe8ae | |||
| 75ce7e91d7 | |||
| 656eea5bb1 | |||
| aba27a7bbf | |||
| 3d7e322bf9 | |||
| 1f3fff4a9d | |||
| 39e06f2b6d | |||
| b775c5b674 | |||
| 796bf37141 | |||
| c5b0e1e876 | |||
| 2e8bd7a8c7 | |||
| 790daba796 | |||
| bcf6b86b84 | |||
| 2e9bd50172 | |||
| c673dce484 | |||
| e1dd8965dc | |||
| 346a0bdc98 | |||
| 8c1169738e | |||
| bc80f91a45 | |||
| 3d062389e9 | |||
| 7afddb5eb5 | |||
| 425a99e5ee | |||
| a5e9339af9 | |||
| 83e20c2dfa | |||
| f85d41945a | |||
| 149eb17b27 | |||
| 72d469b19c | |||
| 6526d7f225 | |||
| 36c0edc62e | |||
| aad26b9ae4 | |||
| 80dcdfe251 | |||
| 1696fc37a8 | |||
| 7759d87835 | |||
| c085de1e9e | |||
| e38359c74f | |||
| 2cfd7e867b | |||
| ec5a2b0cbe | |||
| 2bf426bc22 | |||
| ed8545f8a2 | |||
| 9811492a0b | |||
| 973925b35d | |||
| 0b30cfc341 | |||
| a7e44944fb | |||
| 9b25ebd6cd | |||
| 33b775e263 | |||
| b0edc6302e | |||
| 1a644b131d | |||
| f7689b7a5a | |||
| 1073011a2a | |||
| 6fa0009ebf | |||
| 42c0abedb7 | |||
| 56e59a93f3 | |||
| 9d581f3897 | |||
| e5ac6ec7cd | |||
| 694fe98131 | |||
| 730078597e | |||
| 32ac8db803 | |||
| aad2ed2719 | |||
| a790fed297 | |||
| deb2d5ce2a | |||
| 093097c619 | |||
| 2cdeb7c104 | |||
| 4dbb04bfb8 | |||
| fa8b921635 | |||
| 33dd10c670 | |||
| ba24e1c4f5 |
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: crud-endpoints
|
||||||
|
description: Use whenever asked to add, create, or scaffold a CRUD endpoint, router, or entity in this repo's server (create/list/get/update/delete handlers, new `server/routers/<entity>/` or `server/private/routers/<entity>/` folder). Points to the established file layout, middleware, ActionsEnum, and route-registration conventions before writing any code.
|
||||||
|
---
|
||||||
|
|
||||||
|
Before writing any router/handler/middleware code for a new entity, read
|
||||||
|
`docs/crud-endpoints.md` in full. It documents, with real examples from
|
||||||
|
`server/routers/aiProvider/` (public) and `server/private/routers/alertRule/`
|
||||||
|
(enterprise-only), how this repo structures CRUD endpoints:
|
||||||
|
|
||||||
|
- Directory/file layout per entity (`index.ts`, `types.ts`, `validation.ts`,
|
||||||
|
one file per operation).
|
||||||
|
- The standard handler anatomy (zod parsing, OpenAPI registry, response
|
||||||
|
envelope, error handling).
|
||||||
|
- Where access-control middleware (`verify<Entity>Access`) lives and when
|
||||||
|
it's needed vs. plain `verifyOrgAccess`.
|
||||||
|
- How to wire up `ActionsEnum` entries, `verifyUserHasAction`, and
|
||||||
|
`logActionAudit`.
|
||||||
|
- Which of the four router files (`server/routers/external.ts`,
|
||||||
|
`server/routers/internal.ts`, `server/private/routers/external.ts`,
|
||||||
|
`server/private/routers/internal.ts`) to register routes in, and the
|
||||||
|
middleware chain template per HTTP verb.
|
||||||
|
- The repo's non-standard verb convention: **`PUT` = create, `POST` =
|
||||||
|
update** (backwards from typical REST) — don't "fix" this to standard
|
||||||
|
REST verbs, match the existing convention.
|
||||||
|
- The `#dynamic` import alias, for the rare case of a hook needing different
|
||||||
|
implementations in OSS vs. enterprise builds.
|
||||||
|
|
||||||
|
Follow that doc's checklist (§8) step by step rather than improvising a
|
||||||
|
structure. If the doc and the actual code in `aiProvider`/`alertRule` ever
|
||||||
|
disagree, trust the code and flag the doc as stale.
|
||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
# FROM node:24-slim AS base
|
# FROM node:24.18.1-slim AS base
|
||||||
FROM public.ecr.aws/docker/library/node:24-slim AS base
|
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ FROM base AS builder
|
|||||||
|
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
# FROM node:24-slim AS runner
|
# FROM node:24.18.1-slim AS runner
|
||||||
FROM public.ecr.aws/docker/library/node:24-slim AS runner
|
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24-alpine
|
FROM node:24.18.1-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CommandModule } from "yargs";
|
import { CommandModule } from "yargs";
|
||||||
import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions } from "@server/db";
|
import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions, aiProviders, virtualApiKeys } from "@server/db";
|
||||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -132,12 +132,16 @@ export const rotateServerSecret: CommandModule<
|
|||||||
const certs = await db.select().from(certificates);
|
const certs = await db.select().from(certificates);
|
||||||
const streamingDestinations = await db.select().from(eventStreamingDestinations);
|
const streamingDestinations = await db.select().from(eventStreamingDestinations);
|
||||||
const webhookActions = await db.select().from(alertWebhookActions);
|
const webhookActions = await db.select().from(alertWebhookActions);
|
||||||
|
const providers = await db.select().from(aiProviders);
|
||||||
|
const virtualKeys = await db.select().from(virtualApiKeys);
|
||||||
|
|
||||||
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
|
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
|
||||||
console.log(`Found ${licenseKeys.length} license key(s)`);
|
console.log(`Found ${licenseKeys.length} license key(s)`);
|
||||||
console.log(`Found ${certs.length} certificate(s)`);
|
console.log(`Found ${certs.length} certificate(s)`);
|
||||||
console.log(`Found ${streamingDestinations.length} event streaming destination(s)`);
|
console.log(`Found ${streamingDestinations.length} event streaming destination(s)`);
|
||||||
console.log(`Found ${webhookActions.length} alert webhook action(s)`);
|
console.log(`Found ${webhookActions.length} alert webhook action(s)`);
|
||||||
|
console.log(`Found ${providers.length} AI provider(s)`);
|
||||||
|
console.log(`Found ${virtualKeys.length} virtual API key(s)`);
|
||||||
|
|
||||||
// Prepare all decrypted and re-encrypted values
|
// Prepare all decrypted and re-encrypted values
|
||||||
console.log("\nDecrypting and re-encrypting values...");
|
console.log("\nDecrypting and re-encrypting values...");
|
||||||
@@ -171,11 +175,24 @@ export const rotateServerSecret: CommandModule<
|
|||||||
encryptedConfig: string;
|
encryptedConfig: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AiProviderUpdate = {
|
||||||
|
providerId: number;
|
||||||
|
encryptedApiKey: string | null;
|
||||||
|
encryptedHeaders: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VirtualApiKeyUpdate = {
|
||||||
|
virtualApiKeyId: string;
|
||||||
|
encryptedToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
const idpUpdates: IdpUpdate[] = [];
|
const idpUpdates: IdpUpdate[] = [];
|
||||||
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
|
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
|
||||||
const certUpdates: CertUpdate[] = [];
|
const certUpdates: CertUpdate[] = [];
|
||||||
const streamingDestinationUpdates: StreamingDestinationUpdate[] = [];
|
const streamingDestinationUpdates: StreamingDestinationUpdate[] = [];
|
||||||
const webhookActionUpdates: WebhookActionUpdate[] = [];
|
const webhookActionUpdates: WebhookActionUpdate[] = [];
|
||||||
|
const aiProviderUpdates: AiProviderUpdate[] = [];
|
||||||
|
const virtualApiKeyUpdates: VirtualApiKeyUpdate[] = [];
|
||||||
|
|
||||||
// Process idpOidcConfig entries
|
// Process idpOidcConfig entries
|
||||||
for (const idpConfig of idpConfigs) {
|
for (const idpConfig of idpConfigs) {
|
||||||
@@ -306,6 +323,60 @@ export const rotateServerSecret: CommandModule<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process aiProviders entries (apiKey + headers)
|
||||||
|
for (const provider of providers) {
|
||||||
|
try {
|
||||||
|
if (!provider.apiKey && !provider.headers) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedApiKey = provider.apiKey
|
||||||
|
? encrypt(decrypt(provider.apiKey, oldSecret), newSecret)
|
||||||
|
: null;
|
||||||
|
const encryptedHeaders = provider.headers
|
||||||
|
? encrypt(
|
||||||
|
decrypt(provider.headers, oldSecret),
|
||||||
|
newSecret
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
aiProviderUpdates.push({
|
||||||
|
providerId: provider.providerId,
|
||||||
|
encryptedApiKey,
|
||||||
|
encryptedHeaders
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error processing AI provider ${provider.providerId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process virtualApiKeys entries (token)
|
||||||
|
for (const key of virtualKeys) {
|
||||||
|
try {
|
||||||
|
if (!key.token) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtualApiKeyUpdates.push({
|
||||||
|
virtualApiKeyId: key.virtualApiKeyId,
|
||||||
|
encryptedToken: encrypt(
|
||||||
|
decrypt(key.token, oldSecret),
|
||||||
|
newSecret
|
||||||
|
)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error processing virtual API key ${key.virtualApiKeyId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Perform all database updates in a single transaction
|
// Perform all database updates in a single transaction
|
||||||
console.log("\nUpdating database in transaction...");
|
console.log("\nUpdating database in transaction...");
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
@@ -376,6 +447,32 @@ export const rotateServerSecret: CommandModule<
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update AI provider entries
|
||||||
|
for (const update of aiProviderUpdates) {
|
||||||
|
await trx
|
||||||
|
.update(aiProviders)
|
||||||
|
.set({
|
||||||
|
apiKey: update.encryptedApiKey,
|
||||||
|
headers: update.encryptedHeaders
|
||||||
|
})
|
||||||
|
.where(eq(aiProviders.providerId, update.providerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update virtual API key entries
|
||||||
|
for (const update of virtualApiKeyUpdates) {
|
||||||
|
await trx
|
||||||
|
.update(virtualApiKeys)
|
||||||
|
.set({
|
||||||
|
token: update.encryptedToken
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
virtualApiKeys.virtualApiKeyId,
|
||||||
|
update.virtualApiKeyId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
|
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
|
||||||
@@ -383,6 +480,8 @@ export const rotateServerSecret: CommandModule<
|
|||||||
console.log(`Rotated ${certUpdates.length} certificate(s)`);
|
console.log(`Rotated ${certUpdates.length} certificate(s)`);
|
||||||
console.log(`Rotated ${streamingDestinationUpdates.length} event streaming destination(s)`);
|
console.log(`Rotated ${streamingDestinationUpdates.length} event streaming destination(s)`);
|
||||||
console.log(`Rotated ${webhookActionUpdates.length} alert webhook action(s)`);
|
console.log(`Rotated ${webhookActionUpdates.length} alert webhook action(s)`);
|
||||||
|
console.log(`Rotated ${aiProviderUpdates.length} AI provider(s)`);
|
||||||
|
console.log(`Rotated ${virtualApiKeyUpdates.length} virtual API key(s)`);
|
||||||
|
|
||||||
// Update config file with new secret
|
// Update config file with new secret
|
||||||
console.log("\nUpdating config file...");
|
console.log("\nUpdating config file...");
|
||||||
@@ -402,6 +501,7 @@ export const rotateServerSecret: CommandModule<
|
|||||||
console.log(` - Certificates: ${certUpdates.length}`);
|
console.log(` - Certificates: ${certUpdates.length}`);
|
||||||
console.log(` - Event streaming destinations: ${streamingDestinationUpdates.length}`);
|
console.log(` - Event streaming destinations: ${streamingDestinationUpdates.length}`);
|
||||||
console.log(` - Alert webhook actions: ${webhookActionUpdates.length}`);
|
console.log(` - Alert webhook actions: ${webhookActionUpdates.length}`);
|
||||||
|
console.log(` - AI providers: ${aiProviderUpdates.length}`);
|
||||||
console.log(
|
console.log(
|
||||||
`\n IMPORTANT: Restart the server for the new secret to take effect.`
|
`\n IMPORTANT: Restart the server for the new secret to take effect.`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# AI Gateway Provider Selection
|
||||||
|
|
||||||
|
How the AI gateway picks which attached provider handles a request when an
|
||||||
|
inference resource has more than one AI provider.
|
||||||
|
|
||||||
|
**Code:**
|
||||||
|
|
||||||
|
- Route → capability binding: `server/routers/aiGateway/createAiGatewayRouter.ts`
|
||||||
|
- Request pipeline: `server/routers/aiGateway/pipeline.ts` (`selectProvider`)
|
||||||
|
- Tie-break scoring: `server/lib/aiProviderSelection.ts`
|
||||||
|
- Allow/block matching: `server/lib/aiModelKeyMatch.ts`
|
||||||
|
- Model catalog: `server/lib/aiModelCatalog.ts`
|
||||||
|
- Default capabilities per provider type: `server/lib/aiProviderDefaults.ts`
|
||||||
|
|
||||||
|
Overlapping model allows are permitted at save time. Selection happens at
|
||||||
|
request time. If the algorithm cannot confidently pick one provider, the
|
||||||
|
gateway returns `403` with an ambiguous-provider error.
|
||||||
|
|
||||||
|
## Selection Pipeline
|
||||||
|
|
||||||
|
Every gateway request runs through these steps in order. Each step narrows
|
||||||
|
the candidate set. Later steps only run when more than one provider remains.
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Capability filter
|
||||||
|
2. Allow / block lists
|
||||||
|
3. Most specific allow pattern
|
||||||
|
4. Catalog ownership
|
||||||
|
5. Provider class preference
|
||||||
|
6. Ambiguous → error
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. Capability Filter
|
||||||
|
|
||||||
|
The incoming path selects a capability before any provider logic runs.
|
||||||
|
|
||||||
|
| Path | Capability |
|
||||||
|
|------|------------|
|
||||||
|
| `POST /v1/chat/completions` | `openai_chat` |
|
||||||
|
| `POST /v1/responses` | `openai_responses` |
|
||||||
|
| `POST /v1/messages` | `anthropic_messages` |
|
||||||
|
| Gemini / Vertex / Bedrock routes | their respective capability ids |
|
||||||
|
|
||||||
|
Only attached providers that advertise that capability stay in the candidate
|
||||||
|
set. Default capabilities do not overlap for native OpenAI vs Anthropic:
|
||||||
|
|
||||||
|
| Provider type | Default capabilities |
|
||||||
|
|---------------|----------------------|
|
||||||
|
| `openai` | `openai_chat`, `openai_responses` |
|
||||||
|
| `anthropic` | `anthropic_messages` |
|
||||||
|
| `openRouter` | `openai_chat` |
|
||||||
|
| `vercelAiGateway` | `openai_chat`, `openai_responses` |
|
||||||
|
| `microsoftFoundry` | `openai_chat`, `openai_responses`, `anthropic_messages` |
|
||||||
|
| `custom` | whatever was configured |
|
||||||
|
|
||||||
|
### 2. Allow / Block Lists
|
||||||
|
|
||||||
|
For each remaining provider, the gateway resolves the effective allow and
|
||||||
|
block patterns:
|
||||||
|
|
||||||
|
- **`inherit`**: use the provider's own model lists
|
||||||
|
- **`select`**: use the resource-selected subset of those lists
|
||||||
|
|
||||||
|
A candidate is kept only if `isAllowedByLists(requestedModel, allows, blocks)`
|
||||||
|
passes:
|
||||||
|
|
||||||
|
1. At least one allow pattern must match
|
||||||
|
2. No block pattern may match
|
||||||
|
|
||||||
|
Patterns support `*` and `?` globs (`gpt-*`, `claude-3-5-sonnet-?`).
|
||||||
|
|
||||||
|
### 3. Most Specific Allow Pattern
|
||||||
|
|
||||||
|
Among providers that allow the model, keep those whose matching allow
|
||||||
|
pattern is most specific:
|
||||||
|
|
||||||
|
1. Exact keys beat patterns
|
||||||
|
2. Fewer wildcard characters win
|
||||||
|
3. Longer literal length wins
|
||||||
|
|
||||||
|
Example: `gpt-4o` beats `gpt-*` beats `*`.
|
||||||
|
|
||||||
|
### 4. Catalog Ownership
|
||||||
|
|
||||||
|
When specificity is tied (common with multiple `*` allows), score each
|
||||||
|
provider against the known model catalog:
|
||||||
|
|
||||||
|
| Score | Meaning |
|
||||||
|
|------:|---------|
|
||||||
|
| 2 | Typed provider whose catalog contains the model (`openai` → openai catalog, `anthropic` → anthropic, etc.) |
|
||||||
|
| 1 | Aggregator or custom (`openRouter`, `vercelAiGateway`, `custom`) and the model exists somewhere in the catalog |
|
||||||
|
| 0 | No ownership signal (typed catalog miss, or unknown model on aggregator/custom) |
|
||||||
|
|
||||||
|
Model id lookup tries the raw id, then a stripped `vendor/model` form
|
||||||
|
(e.g. `openai/gpt-4o` → also try `gpt-4o`).
|
||||||
|
|
||||||
|
Typed providers map to catalog providers as:
|
||||||
|
|
||||||
|
| Provider type | Catalog |
|
||||||
|
|---------------|---------|
|
||||||
|
| `openai` | `openai` |
|
||||||
|
| `anthropic` | `anthropic` |
|
||||||
|
| `googleGemini` | `gemini` |
|
||||||
|
| `vertexAi` | `vertex` |
|
||||||
|
| `bedrock` | `bedrock` |
|
||||||
|
| `microsoftFoundry` | `azure` |
|
||||||
|
| `openRouter` / `vercelAiGateway` / `custom` | none (aggregator/custom path) |
|
||||||
|
|
||||||
|
### 5. Provider Class Preference
|
||||||
|
|
||||||
|
If catalog ownership is still tied, prefer:
|
||||||
|
|
||||||
|
| Rank | Class |
|
||||||
|
|-----:|-------|
|
||||||
|
| 2 | Native typed provider (`openai`, `anthropic`, `googleGemini`, …) |
|
||||||
|
| 1 | Aggregator (`openRouter`, `vercelAiGateway`) |
|
||||||
|
| 0 | `custom` |
|
||||||
|
|
||||||
|
### 6. Ambiguous Error
|
||||||
|
|
||||||
|
If more than one distinct provider remains after all steps, the gateway
|
||||||
|
rejects the request:
|
||||||
|
|
||||||
|
```
|
||||||
|
Model "<id>" is ambiguous across multiple AI providers on this resource
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical remaining ties: two OpenAI-type providers both with `*`, or two
|
||||||
|
customs advertising the same capability for an unknown model.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Assume each provider below is attached and enabled on the same inference
|
||||||
|
resource.
|
||||||
|
|
||||||
|
### Example A: OpenAI + Anthropic, Both `*`
|
||||||
|
|
||||||
|
| Provider | Allow | Capabilities |
|
||||||
|
|----------|-------|--------------|
|
||||||
|
| OpenAI | `*` | `openai_chat`, `openai_responses` |
|
||||||
|
| Anthropic | `*` | `anthropic_messages` |
|
||||||
|
|
||||||
|
**Request:** `POST /v1/chat/completions` with `model: "gpt-4o"`
|
||||||
|
|
||||||
|
1. Capability → only OpenAI remains
|
||||||
|
2. Allow → OpenAI matches `*`
|
||||||
|
3. Result → **OpenAI**
|
||||||
|
|
||||||
|
Anthropic never reaches pattern or catalog scoring. Capability alone decides.
|
||||||
|
|
||||||
|
**Request:** `POST /v1/messages` with `model: "claude-3-5-sonnet-latest"`
|
||||||
|
|
||||||
|
1. Capability → only Anthropic remains
|
||||||
|
2. Result → **Anthropic**
|
||||||
|
|
||||||
|
### Example B: OpenAI + OpenRouter, Both `*`
|
||||||
|
|
||||||
|
| Provider | Allow | Capabilities |
|
||||||
|
|----------|-------|--------------|
|
||||||
|
| OpenAI | `*` | `openai_chat`, … |
|
||||||
|
| OpenRouter | `*` | `openai_chat` |
|
||||||
|
|
||||||
|
**Request:** `POST /v1/chat/completions` with `model: "gpt-4o"`
|
||||||
|
|
||||||
|
1. Capability → both remain (`openai_chat`)
|
||||||
|
2. Allow → both match `*`
|
||||||
|
3. Specificity → tie (`*` vs `*`)
|
||||||
|
4. Catalog → OpenAI scores `2` (owns `gpt-4o`); OpenRouter scores `1`
|
||||||
|
5. Result → **OpenAI**
|
||||||
|
|
||||||
|
### Example C: OpenRouter Only Serving a Claude Model Over OpenAI Chat
|
||||||
|
|
||||||
|
| Provider | Allow | Capabilities |
|
||||||
|
|----------|-------|--------------|
|
||||||
|
| OpenRouter | `*` | `openai_chat` |
|
||||||
|
|
||||||
|
**Request:** `POST /v1/chat/completions` with `model: "anthropic/claude-3.5-sonnet"`
|
||||||
|
|
||||||
|
1. Capability → OpenRouter remains
|
||||||
|
2. Only one candidate → **OpenRouter**
|
||||||
|
|
||||||
|
No tie-breaking needed.
|
||||||
|
|
||||||
|
### Example D: OpenAI (`gpt-*`) + OpenRouter (`*`)
|
||||||
|
|
||||||
|
| Provider | Allow |
|
||||||
|
|----------|-------|
|
||||||
|
| OpenAI | `gpt-*` |
|
||||||
|
| OpenRouter | `*` |
|
||||||
|
|
||||||
|
**Request:** `model: "gpt-4o"` on `openai_chat`
|
||||||
|
|
||||||
|
1. Capability → both
|
||||||
|
2. Allow → both match
|
||||||
|
3. Specificity → OpenAI's `gpt-*` beats OpenRouter's `*`
|
||||||
|
4. Result → **OpenAI**
|
||||||
|
|
||||||
|
Catalog scoring is not needed because specificity already unique'd the set.
|
||||||
|
|
||||||
|
### Example E: OpenAI + Anthropic With Overlapping Custom Capabilities
|
||||||
|
|
||||||
|
Someone grants Anthropic `openai_chat` as well (non-default).
|
||||||
|
|
||||||
|
| Provider | Allow | Capabilities |
|
||||||
|
|----------|-------|--------------|
|
||||||
|
| OpenAI | `*` | `openai_chat`, … |
|
||||||
|
| Anthropic | `*` | `anthropic_messages`, `openai_chat` |
|
||||||
|
|
||||||
|
**Request:** `POST /v1/chat/completions` with `model: "gpt-4o"`
|
||||||
|
|
||||||
|
1. Capability → both remain
|
||||||
|
2. Allow → both match `*`
|
||||||
|
3. Specificity → tie
|
||||||
|
4. Catalog → OpenAI `2`, Anthropic `0` (`gpt-4o` is not in the anthropic catalog)
|
||||||
|
5. Result → **OpenAI**
|
||||||
|
|
||||||
|
### Example F: Two Aggregators, Known Model
|
||||||
|
|
||||||
|
| Provider | Allow |
|
||||||
|
|----------|-------|
|
||||||
|
| OpenRouter | `*` |
|
||||||
|
| Vercel AI Gateway | `*` |
|
||||||
|
|
||||||
|
**Request:** `model: "gpt-4o"` on `openai_chat`
|
||||||
|
|
||||||
|
1. Capability → both
|
||||||
|
2. Allow / specificity → tie
|
||||||
|
3. Catalog → both score `1` (known model, no typed owner in the set)
|
||||||
|
4. Class → both aggregators (rank `1`) → still tied
|
||||||
|
5. Result → **ambiguous error**
|
||||||
|
|
||||||
|
Attach a native OpenAI provider (or narrow one aggregator's allow list) to
|
||||||
|
make this determinable.
|
||||||
|
|
||||||
|
### Example G: Two OpenAI Providers, Both `*`
|
||||||
|
|
||||||
|
| Provider | Type | Allow |
|
||||||
|
|----------|------|-------|
|
||||||
|
| OpenAI Prod | `openai` | `*` |
|
||||||
|
| OpenAI Staging | `openai` | `*` |
|
||||||
|
|
||||||
|
**Request:** `model: "gpt-4o"`
|
||||||
|
|
||||||
|
1–5 all leave both candidates (same capability, same specificity, same
|
||||||
|
catalog ownership, same class).
|
||||||
|
|
||||||
|
Result → **ambiguous error**
|
||||||
|
|
||||||
|
Disambiguate with different allow patterns, disable one attachment, or
|
||||||
|
split across resources.
|
||||||
|
|
||||||
|
### Example H: Unknown Model Across Native + Aggregator
|
||||||
|
|
||||||
|
| Provider | Allow |
|
||||||
|
|----------|-------|
|
||||||
|
| OpenAI | `*` |
|
||||||
|
| OpenRouter | `*` |
|
||||||
|
|
||||||
|
**Request:** `model: "my-fine-tune-v3"` (not in catalog)
|
||||||
|
|
||||||
|
1. Capability → both
|
||||||
|
2. Allow / specificity → tie
|
||||||
|
3. Catalog → both score `0` (typed miss + unknown aggregator model)
|
||||||
|
4. Class → OpenAI (`2`) beats OpenRouter (`1`)
|
||||||
|
5. Result → **OpenAI**
|
||||||
|
|
||||||
|
## Practical Guidance
|
||||||
|
|
||||||
|
- Native OpenAI + Anthropic with `*` is safe. Different default APIs never
|
||||||
|
collide.
|
||||||
|
- OpenAI + OpenRouter with `*` is usually fine for catalog-known OpenAI
|
||||||
|
models. Native wins.
|
||||||
|
- Prefer specific allow patterns (`gpt-4o`, `gpt-*`) when two providers share
|
||||||
|
a capability.
|
||||||
|
- Two providers of the same type both using `*` will stay ambiguous. Narrow
|
||||||
|
at least one allow list.
|
||||||
|
- Custom providers only win ties when no stronger native/aggregator signal
|
||||||
|
remains.
|
||||||
|
|
||||||
|
## Related Behavior
|
||||||
|
|
||||||
|
- **Saving providers on a resource does not reject overlapping allows.**
|
||||||
|
Collisions are resolved (or rejected) per request.
|
||||||
|
- Budgets, auth, and upstream URL / target routing run after a single
|
||||||
|
provider has been selected.
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
# How to build a CRUD endpoint in this repo
|
||||||
|
|
||||||
|
Reference for adding a new CRUD entity to the server. Based on two real
|
||||||
|
examples already in the codebase — read them side by side with this doc:
|
||||||
|
|
||||||
|
- **Public / open-source (Community Edition) pattern**: `server/routers/aiProvider/`
|
||||||
|
- **Enterprise-only pattern**: `server/private/routers/alertRule/`
|
||||||
|
|
||||||
|
The two are structurally identical. The only difference is *where the files
|
||||||
|
live* and *which router they get wired into*.
|
||||||
|
|
||||||
|
## 1. Decide: public or private?
|
||||||
|
|
||||||
|
- `server/routers/<entity>/` — ships in the open-source Community Edition.
|
||||||
|
Anyone running Pangolin gets this.
|
||||||
|
- `server/private/routers/<entity>/` — Enterprise/SaaS only. Gated behind
|
||||||
|
`verifyValidLicense` (and often `verifyValidSubscription(tierMatrix.x)`).
|
||||||
|
Every file here starts with the Fossorial Commercial License header block
|
||||||
|
(copy it verbatim from an existing private file).
|
||||||
|
|
||||||
|
Everything below applies to both — swap `@server/...` for `#private/...`
|
||||||
|
import paths and add license headers when building the private version.
|
||||||
|
|
||||||
|
## 2. Directory layout
|
||||||
|
|
||||||
|
One folder per entity, one file per operation, a barrel `index.ts`:
|
||||||
|
|
||||||
|
```
|
||||||
|
server/routers/<entity>/
|
||||||
|
index.ts # export * from each operation file + ./types
|
||||||
|
types.ts # response payload types + row->public mapper
|
||||||
|
validation.ts # zod schemas/refinements shared by create + update (optional)
|
||||||
|
create<Entity>.ts
|
||||||
|
list<Entities>.ts
|
||||||
|
get<Entity>.ts
|
||||||
|
update<Entity>.ts
|
||||||
|
delete<Entity>.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
`index.ts` is a flat barrel:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export * from "./createAiProvider";
|
||||||
|
export * from "./listAiProviders";
|
||||||
|
export * from "./getAiProvider";
|
||||||
|
export * from "./updateAiProvider";
|
||||||
|
export * from "./deleteAiProvider";
|
||||||
|
export * from "./types";
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Anatomy of a single handler
|
||||||
|
|
||||||
|
Every handler file (`create<Entity>.ts`, etc.) follows the same shape:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { <table>, db } from "@server/db";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { GetXResponse } from "@server/routers/<entity>/types";
|
||||||
|
|
||||||
|
const paramsSchema = z.strictObject({
|
||||||
|
orgId: z.string().nonempty() // or entityId: z.coerce.number().int().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodySchema = z.strictObject({ /* ... */ }); // create/update only
|
||||||
|
|
||||||
|
registry.registerPath({
|
||||||
|
method: "get", // put | post | delete
|
||||||
|
path: "/org/{orgId}/x",
|
||||||
|
description: "...",
|
||||||
|
tags: [OpenAPITags.<Entity>],
|
||||||
|
request: { params: paramsSchema, /* body: {...} for write ops, query: for list */ },
|
||||||
|
responses: { 200: { description: "Successful response" } }
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function getX(req: Request, res: Response, next: NextFunction): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedParams = paramsSchema.safeParse(req.params);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(createHttpError(HttpCode.BAD_REQUEST, fromError(parsedParams.error).toString()));
|
||||||
|
}
|
||||||
|
// parse body too, if present, same pattern
|
||||||
|
|
||||||
|
// ...business logic against db...
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return next(createHttpError(HttpCode.NOT_FOUND, `X with ID ${id} not found`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<GetXResponse>(res, {
|
||||||
|
data: { /* ... */ },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "X retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules to keep consistent with the rest of the codebase:
|
||||||
|
|
||||||
|
- `z.strictObject` for params/body — rejects unknown keys.
|
||||||
|
- Params parsed first, then body; each on its own `safeParse` + early
|
||||||
|
`next(createHttpError(...))` — never throw raw errors.
|
||||||
|
- Every handler registers itself with the OpenAPI `registry` even if nobody
|
||||||
|
reads the spec directly — it's how `/api/v1/docs` stays accurate.
|
||||||
|
- Catch-all `try/catch` at the bottom: `logger.error(error)` +
|
||||||
|
generic `500` message. Never leak internal error details to the client.
|
||||||
|
- Use `response<T>(res, { data, success, error, message, status })` from
|
||||||
|
`@server/lib/response` for every response, success or otherwise (errors go
|
||||||
|
through `next(createHttpError(...))` instead, not through `response`).
|
||||||
|
- If the route already ran an access-control middleware that fetched the row
|
||||||
|
(see §5), reuse it instead of re-querying:
|
||||||
|
`req.aiProvider && req.aiProvider.providerId === providerId ? [req.aiProvider] : await db.select()...`
|
||||||
|
|
||||||
|
### List handler specifics
|
||||||
|
|
||||||
|
Pagination is a fixed shape (`page`, `pageSize`, optional `query` for
|
||||||
|
search). See `listAiProviders.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const listSchema = z.object({
|
||||||
|
pageSize: z.coerce.number<string>().int().positive().optional().catch(20).default(20),
|
||||||
|
page: z.coerce.number<string>().int().min(0).optional().catch(1).default(1),
|
||||||
|
query: z.string().optional()
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the count query and the page query in `Promise.all`, and return
|
||||||
|
`PaginatedResponse<{ items: T[] }>` (`@server/types/Pagination`) with
|
||||||
|
`{ total, pageSize, page }`.
|
||||||
|
|
||||||
|
### types.ts specifics
|
||||||
|
|
||||||
|
- Define one response type per operation: `List<Entities>Response`,
|
||||||
|
`Get<Entity>Response`, `CreateOrEdit<Entity>Response` (create and update
|
||||||
|
commonly share a response shape).
|
||||||
|
- If the raw DB row needs to be shaped for clients (decrypting secrets,
|
||||||
|
parsing a serialized column, hiding a column), put a `toPublic<Entity>()`
|
||||||
|
mapper here — see `toPublicAiProvider` for the pattern of stripping
|
||||||
|
`apiKey`/serialized columns and re-adding decrypted/parsed versions.
|
||||||
|
|
||||||
|
### validation.ts specifics
|
||||||
|
|
||||||
|
Only needed when create and update share non-trivial zod pieces (enums,
|
||||||
|
`superRefine` cross-field rules). Export the raw schemas (`z.enum([...])`)
|
||||||
|
and refinement functions, and import them into both `createX.ts` and
|
||||||
|
`updateX.ts` — see `aiProvider/validation.ts`'s
|
||||||
|
`refineProviderUpstreamFields`.
|
||||||
|
|
||||||
|
## 4. Wire up an access-control middleware (for id-scoped routes)
|
||||||
|
|
||||||
|
For routes scoped to a single row (`/x/:xId`, as opposed to
|
||||||
|
`/org/:orgId/x` create/list), add a `verify<Entity>Access` middleware in
|
||||||
|
`server/middlewares/` (or `server/private/middlewares/` for enterprise-only
|
||||||
|
entities) and export it from that directory's `index.ts`.
|
||||||
|
|
||||||
|
Pattern (`verifyAiProviderAccess.ts`):
|
||||||
|
|
||||||
|
1. Read the id param, `Number.parseInt`/validate it.
|
||||||
|
2. Load the row by id.
|
||||||
|
3. `404` if it doesn't exist.
|
||||||
|
4. Resolve the row's `orgId`, then check/attach `req.userOrg` (query
|
||||||
|
`userOrgs` if not already on the request), `403` if the user isn't in
|
||||||
|
that org.
|
||||||
|
5. Run `checkOrgAccessPolicy` if `req.orgPolicyAllowed` hasn't been resolved
|
||||||
|
yet.
|
||||||
|
6. Set `req.userOrgId`, `req.userOrgRoleIds`, and stash the row on the
|
||||||
|
request (e.g. `req.aiProvider = provider`) so downstream handlers and
|
||||||
|
`verifyUserHasAction` don't have to refetch it.
|
||||||
|
|
||||||
|
Org-scoped create/list routes (`/org/:orgId/x`) don't need a bespoke
|
||||||
|
middleware — they use the existing generic `verifyOrgAccess` from
|
||||||
|
`@server/middlewares`.
|
||||||
|
|
||||||
|
## 5. Register an action + permission check
|
||||||
|
|
||||||
|
Add one `ActionsEnum` entry per operation in `server/auth/actions.ts`,
|
||||||
|
grouped near the entity's other actions, named `create<Entity>`,
|
||||||
|
`get<Entity>`, `update<Entity>`, `delete<Entity>`, `list<Entities>`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
createAiProvider = "createAiProvider",
|
||||||
|
deleteAiProvider = "deleteAiProvider",
|
||||||
|
getAiProvider = "getAiProvider",
|
||||||
|
listAiProviders = "listAiProviders",
|
||||||
|
updateAiProvider = "updateAiProvider",
|
||||||
|
```
|
||||||
|
|
||||||
|
Every route uses `verifyUserHasAction(ActionsEnum.x)` to check the caller's
|
||||||
|
role/permissions for that action, and mutating routes (create/update/delete)
|
||||||
|
follow it with `logActionAudit(ActionsEnum.x)` to record the action in the
|
||||||
|
audit log.
|
||||||
|
|
||||||
|
## 6. Register the routes
|
||||||
|
|
||||||
|
There are four router files; which one(s) you touch depends on public vs.
|
||||||
|
private and user-facing vs. service-to-service:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `server/routers/external.ts` | Public, user-facing API. Exports `authenticated`, `unauthenticated`, `authRouter` Express routers. |
|
||||||
|
| `server/routers/internal.ts` | Public, internal service-to-service API (gerbil, badger, traefik-config) — no user auth, exports `internalRouter`. |
|
||||||
|
| `server/private/routers/external.ts` | Enterprise-only, user-facing. Imports `authenticated`/`unauthenticated`/`authRouter` **from the public `external.ts`** and re-exports them, then adds more routes on top. |
|
||||||
|
| `server/private/routers/internal.ts` | Enterprise-only, service-to-service. Same re-export trick with `internalRouter`. |
|
||||||
|
|
||||||
|
Private router files always start:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
unauthenticated as ua,
|
||||||
|
authenticated as a,
|
||||||
|
authRouter as aa
|
||||||
|
} from "@server/routers/external";
|
||||||
|
|
||||||
|
export const authenticated = a;
|
||||||
|
export const unauthenticated = ua;
|
||||||
|
export const authRouter = aa;
|
||||||
|
```
|
||||||
|
|
||||||
|
...and then call `authenticated.get/put/post/delete(...)` to bolt on
|
||||||
|
additional, enterprise-only routes on the *same* router instances the public
|
||||||
|
build uses. This is why the private build has strictly more routes than the
|
||||||
|
public build, not a divergent copy.
|
||||||
|
|
||||||
|
### Route registration order (mutating vs read)
|
||||||
|
|
||||||
|
Standard middleware chain per verb, using `alertRule`'s registrations as the
|
||||||
|
template:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Create — org-scoped, no row exists yet
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/x",
|
||||||
|
verifyValidLicense, // private/enterprise routes only
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyLimits, // if the entity counts against a plan limit
|
||||||
|
verifyUserHasAction(ActionsEnum.createX),
|
||||||
|
logActionAudit(ActionsEnum.createX),
|
||||||
|
x.createX
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update — row-scoped
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/x/:xId", // or "/x/:xId" if id is globally unique
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess, // or verifyXAccess if globally-keyed
|
||||||
|
verifyUserHasAction(ActionsEnum.updateX),
|
||||||
|
logActionAudit(ActionsEnum.updateX),
|
||||||
|
x.updateX
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete — row-scoped
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/x/:xId",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.deleteX),
|
||||||
|
logActionAudit(ActionsEnum.deleteX),
|
||||||
|
x.deleteX
|
||||||
|
);
|
||||||
|
|
||||||
|
// List — org-scoped, read-only, no audit log
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/xs",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.listXs),
|
||||||
|
x.listXs
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get one — row-scoped, read-only, no audit log
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/x/:xId",
|
||||||
|
verifyValidLicense,
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getX),
|
||||||
|
x.getX
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- HTTP verbs: `PUT` = create, `POST` = update, `GET` = read, `DELETE` =
|
||||||
|
delete. This repo does not use `PATCH` for entity updates (site
|
||||||
|
provisioning keys are the one exception, using `PATCH`).
|
||||||
|
- `verifyValidLicense` is only needed on private/enterprise routes; public
|
||||||
|
OSS routes skip it.
|
||||||
|
- Use `verifyValidSubscription(tierMatrix.someFeature)` right after
|
||||||
|
`verifyValidLicense` when a feature is gated to specific SaaS tiers (see
|
||||||
|
`tierMatrix` usages in `server/private/routers/external.ts`).
|
||||||
|
- `verifyLimits` goes on create routes for entities that count against a
|
||||||
|
plan/seat limit.
|
||||||
|
- For entities keyed by a globally-unique id (not nested under `/org/:orgId`),
|
||||||
|
use the dedicated `verify<Entity>Access` middleware from §4 instead of
|
||||||
|
`verifyOrgAccess` on the row-scoped routes (see how `/ai-provider/:providerId`
|
||||||
|
uses `verifyAiProviderAccess`, while `/org/:orgId/ai-provider` create/list
|
||||||
|
use plain `verifyOrgAccess`).
|
||||||
|
- Read-only routes (`get`, `list`) skip `logActionAudit` — only mutations are
|
||||||
|
audited.
|
||||||
|
- `internal*.ts` routes are for trusted internal callers (gerbil/badger
|
||||||
|
sidecars) and generally skip user-facing auth entirely, using
|
||||||
|
`verifySessionUserMiddleware` / `verifyUserFromResourceSessionMiddleware`
|
||||||
|
instead of `verifyOrgAccess`/`verifyUserHasAction`. CRUD entities almost
|
||||||
|
never need internal router entries — only add one if a sidecar process
|
||||||
|
needs direct access to the resource.
|
||||||
|
|
||||||
|
## 7. The `#dynamic` alias (advanced — most CRUD work can ignore this)
|
||||||
|
|
||||||
|
Some middleware (e.g. `logActionAudit`) needs a real implementation in the
|
||||||
|
enterprise/SaaS build but a no-op stub in the open-source build, while
|
||||||
|
being imported by identical code in `server/routers/external.ts` in both
|
||||||
|
builds. That's done via the `#dynamic/*` import alias, which
|
||||||
|
`tsconfig.oss.json` points at `./server/*` and `tsconfig.enterprise.json` /
|
||||||
|
`tsconfig.saas.json` point at `./server/private/*`. You only need this
|
||||||
|
pattern if you're adding a genuinely dual-implementation hook; a normal
|
||||||
|
private-only CRUD entity (like `alertRule`) never touches `#dynamic` — it
|
||||||
|
just lives entirely under `server/private/` and is imported with `#private/*`
|
||||||
|
directly from `server/private/routers/external.ts`.
|
||||||
|
|
||||||
|
## 8. Checklist for a new entity
|
||||||
|
|
||||||
|
1. Add the DB table to `server/db/pg/schema/schema.ts` (and sqlite schema if
|
||||||
|
applicable).
|
||||||
|
2. Add `ActionsEnum` entries in `server/auth/actions.ts`.
|
||||||
|
3. Create `server/routers/<entity>/` (or `server/private/routers/<entity>/`):
|
||||||
|
`types.ts`, optional `validation.ts`, one file per operation, `index.ts`
|
||||||
|
barrel.
|
||||||
|
4. If routes are row-scoped by a global id, add
|
||||||
|
`verify<Entity>Access.ts` to `server/middlewares/` or
|
||||||
|
`server/private/middlewares/`, and export it from that directory's
|
||||||
|
`index.ts`.
|
||||||
|
5. Wire routes into `external.ts` (public or private) following the verb/
|
||||||
|
middleware table in §6. Add to `internal.ts` only if a sidecar needs
|
||||||
|
direct access.
|
||||||
|
6. Add license header block to every new file if it's under `server/private/`.
|
||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Моля, уверете се, че сте влезли като правилния потребител.",
|
"inviteLoginUser": "Моля, уверете се, че сте влезли като правилния потребител.",
|
||||||
"inviteErrorNoUser": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е за съществуващ потребител.",
|
"inviteErrorNoUser": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е за съществуващ потребител.",
|
||||||
"inviteCreateUser": "Моля, първо създайте акаунт.",
|
"inviteCreateUser": "Моля, първо създайте акаунт.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Поканите могат да се приемат само от вътрешни акаунти. Отпишете се и влезте с вашата парола за този имейл.",
|
||||||
|
"inviteLoginInternalOnly": "Поканите изискват вътрешен акаунт с парола. Създайте акаунт или влезте с вашата парола.",
|
||||||
"goHome": "Отиди вкъщи",
|
"goHome": "Отиди вкъщи",
|
||||||
"inviteLogInOtherUser": "Влезте като друг потребител",
|
"inviteLogInOtherUser": "Влезте като друг потребител",
|
||||||
"createAnAccount": "Създайте профил",
|
"createAnAccount": "Създайте профил",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Не е намерена държава.",
|
"noCountryFound": "Не е намерена държава.",
|
||||||
"siteSelectionDescription": "Този сайт ще осигури свързаност до целта.",
|
"siteSelectionDescription": "Този сайт ще осигури свързаност до целта.",
|
||||||
"resourceType": "Тип ресурс",
|
"resourceType": "Тип ресурс",
|
||||||
"resourceTypeDescription": "Това контролира протокола на ресурса и как той ще се визуализира в браузъра. Това не може да бъде променено по-късно.",
|
"resourceTypeDescription": "Това контролира протокола на ресурса и не може да се промени по-късно.",
|
||||||
|
"resourceTypeSearch": "Търсене на типове ресурси...",
|
||||||
|
"resourceTypeNotFound": "Не е намерен тип ресурс",
|
||||||
|
"resourceTypeHttpDescription": "Прокси на уеб трафик през HTTPS, използвайки име на домейн",
|
||||||
|
"resourceTypeInferenceDescription": "Направа на AI заявки чрез прикрепени доставчици",
|
||||||
|
"resourceTypeSshDescription": "Достъп до SSH сървър от уеб браузъра",
|
||||||
|
"resourceTypeRdpDescription": "Достъп до отдалечен работен плот от уеб браузъра",
|
||||||
|
"resourceTypeVncDescription": "Достъп до VNC десктоп от уеб браузъра",
|
||||||
|
"resourceTypeTcpDescription": "Прокси на суров TCP трафик, използвайки номер на порт",
|
||||||
|
"resourceTypeUdpDescription": "Прокси на суров UDP трафик, използвайки номер на порт",
|
||||||
|
"privateResourceTypeDescription": "Това контролира как клиентите достигат до ресурса. Това не може да бъде променено по-късно.",
|
||||||
|
"privateResourceTypeHostDescription": "Излагане на единичен хост в мрежата на сайта към свързани клиенти",
|
||||||
|
"privateResourceTypeCidrDescription": "Излагане на CIDR обхват в мрежата на сайта към свързани клиенти",
|
||||||
|
"privateResourceTypeHttpDescription": "Достъп до HTTP или HTTPS услуга чрез домейн",
|
||||||
|
"privateResourceTypeSshDescription": "Достъп до SSH сървър от свързани клиенти",
|
||||||
"resourceDomainDescription": "Ресурсът ще се обслужва на това напълно квалифицирано име на домейн.",
|
"resourceDomainDescription": "Ресурсът ще се обслужва на това напълно квалифицирано име на домейн.",
|
||||||
"resourceHTTPSSettings": "HTTPS настройки",
|
"resourceHTTPSSettings": "HTTPS настройки",
|
||||||
"resourceHTTPSSettingsDescription": "Конфигурирайте как ресурсът ще бъде достъпен по HTTPS",
|
"resourceHTTPSSettingsDescription": "Конфигурирайте как ресурсът ще бъде достъпен по HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Запази Настройки",
|
"authMethodsSave": "Запази Настройки",
|
||||||
"policyAuthStackTitle": "Удостоверяване",
|
"policyAuthStackTitle": "Удостоверяване",
|
||||||
"policyAuthStackDescription": "Контролирайте кои методи за удостоверяване са необходими за достъп до този ресурс",
|
"policyAuthStackDescription": "Контролирайте кои методи за удостоверяване са необходими за достъп до този ресурс",
|
||||||
|
"policyAuthInferenceStackDescription": "Изберете кои потребители и роли могат да се удостоверят с този AI шлюз",
|
||||||
"policyAuthOrLogicTitle": "Множество активни методи за удостоверяване",
|
"policyAuthOrLogicTitle": "Множество активни методи за удостоверяване",
|
||||||
"policyAuthOrLogicBanner": "Посетителите могат да се удостоверяват чрез който и да е от активните методи по-долу. Не е необходимо да преминават през всичките.",
|
"policyAuthOrLogicBanner": "Посетителите могат да се удостоверяват чрез който и да е от активните методи по-долу. Не е необходимо да преминават през всичките.",
|
||||||
"policyAuthMethodActive": "Активен",
|
"policyAuthMethodActive": "Активен",
|
||||||
"policyAuthMethodOff": "Изключено",
|
"policyAuthMethodOff": "Изключено",
|
||||||
"policyAuthSsoTitle": "Платформено SSO",
|
"policyAuthSsoTitle": "Платформено SSO",
|
||||||
"policyAuthSsoDescription": "Изисква вход чрез удостоверител на вашата организация",
|
"policyAuthSsoDescription": "Изисква вход чрез удостоверител на вашата организация",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "URL за вход",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Всеки потребител вече има клавиш за API идентичност, така че трябва да създадете виртуални API ключове само за клиенти, които не са потребители или за споделен достъп. Потребителите могат да получат своя ключ, като влязат с доставчик на идентичност на URL адреса на този ресурс, където ще бъде показан след вход.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Всеки потребител вече има клавиш за API идентичност, така че трябва да създадете виртуални API ключове само за клиенти, които не са потребители или за споделен достъп. Потребителите получават своя ключ тук след влизане с доставчик на идентичност.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} потребители, {roles} роли",
|
"policyAuthSsoSummary": "{idp} · {users} потребители, {roles} роли",
|
||||||
"policyAuthSsoDefaultIdp": "По подразбиране удостоверител",
|
"policyAuthSsoDefaultIdp": "По подразбиране удостоверител",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Добавете удостоверител по подразбиране",
|
"policyAuthAddDefaultIdentityProvider": "Добавете удостоверител по подразбиране",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Нова роля е необходима",
|
"accessRoleErrorNewRequired": "Нова роля е необходима",
|
||||||
"accessRoleErrorRemove": "Неуспешно премахване на роля",
|
"accessRoleErrorRemove": "Неуспешно премахване на роля",
|
||||||
"accessRoleErrorRemoveDescription": "Възникна грешка при премахването на роля.",
|
"accessRoleErrorRemoveDescription": "Възникна грешка при премахването на роля.",
|
||||||
|
"accessRoleInferenceBudget": "AI Бюджет",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Конфигурирайте как членовете на тази роля ограничават използването на AI въз основа на лимити на разходи или жетони",
|
||||||
"accessRoleName": "Име на роля",
|
"accessRoleName": "Име на роля",
|
||||||
"accessRoleQuestionRemove": "Ще изтриете ролята `{name}`. Не можете да отмените това действие.",
|
"accessRoleQuestionRemove": "Ще изтриете ролята `{name}`. Не можете да отмените това действие.",
|
||||||
"accessRoleRemove": "Премахни роля",
|
"accessRoleRemove": "Премахни роля",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Изтриване на сайта",
|
"actionDeleteSite": "Изтриване на сайта",
|
||||||
"actionGetSite": "Вземете сайт",
|
"actionGetSite": "Вземете сайт",
|
||||||
"actionListSites": "Изброяване на сайтове",
|
"actionListSites": "Изброяване на сайтове",
|
||||||
|
"actionCreateAiProvider": "Създаване на AI Доставчик",
|
||||||
|
"actionDeleteAiProvider": "Изтриване на AI Доставчик",
|
||||||
|
"actionGetAiProvider": "Получаване на AI Доставчик",
|
||||||
|
"actionListAiProviders": "Списък на AI Доставчици",
|
||||||
|
"actionUpdateAiProvider": "Актуализиране на AI Доставчик",
|
||||||
|
"actionCreateAiModel": "Създаване на AI Модел",
|
||||||
|
"actionDeleteAiModel": "Изтриване на AI Модел",
|
||||||
|
"actionGetAiModel": "Получаване на AI Модел",
|
||||||
|
"actionListAiModels": "Списък на AI Модели",
|
||||||
|
"actionUpdateAiModel": "Актуализиране на AI Модел",
|
||||||
|
"actionCreateAiBudget": "Създаване на AI Бюджет",
|
||||||
|
"actionDeleteAiBudget": "Изтриване на AI Бюджет",
|
||||||
|
"actionGetAiBudget": "Получаване на AI Бюджет",
|
||||||
|
"actionListAiBudgets": "Списък на AI Бюджети",
|
||||||
|
"actionUpdateAiBudget": "Актуализиране на AI Бюджет",
|
||||||
|
"actionListResourceAiModels": "Списък на модели на AI ресурс",
|
||||||
|
"actionSetResourceAiModels": "Задаване на модели на AI ресурс",
|
||||||
|
"actionCreateVirtualApiKey": "Създаване на Виртуален API Ключ",
|
||||||
|
"actionDeleteVirtualApiKey": "Изтриване на Виртуален API Ключ",
|
||||||
|
"actionGetVirtualApiKey": "Получаване на Виртуален API Ключ",
|
||||||
|
"actionListVirtualApiKeys": "Списък на Виртуални API Ключове",
|
||||||
|
"actionUpdateVirtualApiKey": "Актуализиране на Виртуален API Ключ",
|
||||||
"actionApplyBlueprint": "Приложи Чернова",
|
"actionApplyBlueprint": "Приложи Чернова",
|
||||||
"actionListBlueprints": "Списък с планове.",
|
"actionListBlueprints": "Списък с планове.",
|
||||||
"actionGetBlueprint": "Вземи план.",
|
"actionGetBlueprint": "Вземи план.",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Задайте ПИН код на ресурса",
|
"actionSetResourcePincode": "Задайте ПИН код на ресурса",
|
||||||
"actionSetResourceEmailWhitelist": "Задайте списък на одобрените имейл адреси за ресурса",
|
"actionSetResourceEmailWhitelist": "Задайте списък на одобрените имейл адреси за ресурса",
|
||||||
"actionGetResourceEmailWhitelist": "Вземете списък на одобрените имейл адреси за ресурса",
|
"actionGetResourceEmailWhitelist": "Вземете списък на одобрените имейл адреси за ресурса",
|
||||||
|
"actionListResourcePolicies": "Списък на ресурсни политики",
|
||||||
|
"actionCreateResourcePolicy": "Създаване на ресурсна политика",
|
||||||
"actionGetResourcePolicy": "Получете политика за ресурси",
|
"actionGetResourcePolicy": "Получете политика за ресурси",
|
||||||
"actionUpdateResourcePolicy": "Актуализирайте политика за ресурси",
|
"actionUpdateResourcePolicy": "Актуализирайте политика за ресурси",
|
||||||
|
"actionDeleteResourcePolicy": "Изтриване на ресурсна политика",
|
||||||
"actionSetResourcePolicyUsers": "Задайте потребители на ресурсна политика",
|
"actionSetResourcePolicyUsers": "Задайте потребители на ресурсна политика",
|
||||||
"actionSetResourcePolicyRoles": "Задайте роли на ресурсна политика",
|
"actionSetResourcePolicyRoles": "Задайте роли на ресурсна политика",
|
||||||
"actionSetResourcePolicyPassword": "Задайте парола на ресурсна политика",
|
"actionSetResourcePolicyPassword": "Задайте парола на ресурсна политика",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Покани",
|
"sidebarInvitations": "Покани",
|
||||||
"sidebarRoles": "Роли",
|
"sidebarRoles": "Роли",
|
||||||
"sidebarShareableLinks": "Споделими връзки",
|
"sidebarShareableLinks": "Споделими връзки",
|
||||||
|
"sidebarAiGateway": "AI Шлюз",
|
||||||
|
"sidebarAiProviders": "Доставчици",
|
||||||
|
"commandAiProviders": "AI Доставчици",
|
||||||
|
"sidebarVirtualApiKeys": "Виртуални API Ключове",
|
||||||
|
"sidebarMyApiKeys": "Вашите API Ключове",
|
||||||
|
"sidebarAccount": "Стартер",
|
||||||
|
"commandVirtualApiKeys": "Виртуални API Ключове",
|
||||||
|
"virtualApiKeysTitle": "Управление на Виртуални API Ключове",
|
||||||
|
"virtualApiKeysDescription": "Създаване и управление на ръчни API ключове за достъп до AI шлюза до публични AI шлюзове",
|
||||||
|
"virtualApiKeysTabIdentity": "Идентификационни ключове",
|
||||||
|
"virtualApiKeysTabVirtual": "Виртуални ключове",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Идентификационни ключове за всеки потребител",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Всеки потребител вече има клавиш за идентичност Pangolin за тази организация. Той е уникален за неговия профил и го удостоверява при достъп до AI шлюзове.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Пример",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Как получават своя ключ потребителите",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Посетете публичен AI шлюз URL адрес в браузъра и влезте с профила си.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Или отидете на <url></url> докато сте влезли.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Можете да създадете допълнителни виртуални API ключове в таба Виртуални ключове. Тези ключове могат да бъдат обхванати до конкретни публични AI шлюзове и при необходимост свързани с потребител. Създаването на ключ веднага предоставя достъп до избраните публични AI шлюзове.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Ръчно създаване на ключ",
|
||||||
|
"virtualApiKeysEmailIdentity": "Имейл идентификационни ключове",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Изпратете на всеки избран потребител или роля неговия ключ за идентичност Pangolin.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Изпратете на всички потребители",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Изпратете имейл на всеки член на организацията, който има акаунт имейл.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Потребители",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Роли",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Изпращане на имейли",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Идентификационни ключове изпратени",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Изпратени {sent} имейли.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} потребители бяха прескочени, защото нямат имейл адрес.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Изберете поне един потребител или роля, или изпратете на всички потребители.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Грешка при изпращането на идентификационни ключове",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Неуспех при изпращането на идентификационни ключове",
|
||||||
|
"virtualApiKeysBannerTitle": "Идентификационни ключове за всеки потребител",
|
||||||
|
"virtualApiKeysBannerDescription": "Всеки потребител вече има наличен идентификационен ключ на {keysUrl}. Можете също така да генерирате ръчно ключове тук, които предоставят директен достъп до публични AI шлюзове.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Преглеждане на идентификационни ключове",
|
||||||
|
"virtualApiKeys": "Виртуални API Ключове",
|
||||||
|
"virtualApiKeysSearch": "Търсене на ключове...",
|
||||||
|
"virtualApiKeysCreate": "Създаване на Виртуален API Ключ",
|
||||||
|
"virtualApiKeysCreateDescription": "Секретен ключ за всички публични AI шлюзове в тази организация",
|
||||||
|
"virtualApiKeysCreateButton": "Създаване на ключ",
|
||||||
|
"virtualApiKeysEmpty": "Все още няма Виртуални API Ключове",
|
||||||
|
"virtualApiKeysName": "Име",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Описание (по избор)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Свързване на потребител (по избор)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Свържете този ключ с потребител, за да проследя появата. След създаването този ключ веднага предоставя достъп до избраните публични AI шлюзове. Не е необходимо да свързвате потребител ръчно, за да бъде видим ресурсът. Ключът ще се появи и в профила на потребителя.",
|
||||||
|
"virtualApiKeysAllResources": "Всички публични AI шлюзове",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Позволете на този ключ да има достъп до всеки публичен AI шлюз в организацията",
|
||||||
|
"virtualApiKeysSelectResources": "Публични AI шлюзове",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Избор на ресурси",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Изберете кои публични AI шлюзове този ключ може да има достъп",
|
||||||
|
"virtualApiKeysNoResources": "Няма ресурси",
|
||||||
|
"virtualApiKeysSecret": "Ключ",
|
||||||
|
"virtualApiKeysCopyKey": "Копирайте този ключ. Можете да го видите отново по-късно от таблицата или при редактиране.",
|
||||||
|
"virtualApiKeysViewSecret": "Преглед на секрет",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Секрет за Виртуален API Ключ",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Този секрет предоставя достъп до публичните AI шлюзове, присвоени на този ключ",
|
||||||
|
"virtualApiKeysEdit": "Редактиране на Виртуален API Ключ",
|
||||||
|
"virtualApiKeysEditDescription": "Актуализирайте свързвания потребител и достъпа до публичния AI шлюз за този ключ",
|
||||||
|
"virtualApiKeysSaveButton": "Запазване на промените",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Изберете поне един публичен AI шлюз, или активирайте всички публични AI шлюзове",
|
||||||
|
"virtualApiKeysUpdated": "Виртуален API ключ е актуализиран",
|
||||||
|
"virtualApiKeysUpdatedDescription": "Виртуалният API ключ е актуализиран",
|
||||||
|
"virtualApiKeysErrorUpdate": "Грешка при актуализация на виртуален API ключ",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Неуспех при актуализация на виртуален API ключ",
|
||||||
|
"virtualApiKeysErrorCreate": "Грешка при създаване на виртуален API ключ",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Неуспех при създаване на виртуален API ключ",
|
||||||
|
"virtualApiKeysErrorDelete": "Грешка при изтриване на виртуален API ключ",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Неуспех при изтриването на виртуален API ключ",
|
||||||
|
"virtualApiKeysDeleted": "Виртуален API ключ е изтрит",
|
||||||
|
"virtualApiKeysDeletedDescription": "Виртуалният API ключ е изтрит",
|
||||||
|
"virtualApiKeysDelete": "Изтриване на Виртуален API Ключ",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Изтриване на ключ",
|
||||||
|
"virtualApiKeysQuestionRemove": "Сигурни ли сте, че искате да изтриете този виртуален API ключ?",
|
||||||
|
"virtualApiKeysMessageRemove": "Клиентите, които използват този ключ, ще загубят достъп веднага.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Грешка при зареждане на секрет",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Неуспех при зареждането на секрета на виртуалния API ключ",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Неопределен",
|
||||||
|
"virtualApiKeysInferenceBudget": "Бюджет",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Конфигуриране как този ключ ограничава използването на AI въз основа на лимити на разходи или жетони",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Имейл ключ при генериране",
|
||||||
|
"virtualApiKeysEmailThisKey": "Имейл този ключ",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Изпратете ключа на свързания потребител и допълнителни адреси след създаването му",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Изпратете текущия ключ на свързания потребител и допълнителни адреси",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "Имейлът не е конфигуриран на този сървър",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Конфигурирайте SMTP за изпращане на виртуални API ключове.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Изпратете на свързан потребител",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Изпратете ключът на акаунтния имейл на свързания потребител",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Свържете потребител за изпращане на ключа на този потребител",
|
||||||
|
"virtualApiKeysEmailAdditional": "Допълнителни имейли",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Добавете имейл и натиснете Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Изберете свързания потребител или добавете поне един имейл адрес",
|
||||||
|
"myVirtualApiKeysTitle": "Вашите API Ключове",
|
||||||
|
"myVirtualApiKeysDescription": "Прегледайте вашия идентификационен ключ и всички виртуални API ключове, които ви се отнасят в тази организация",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Вашите API Ключове за {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Прегледайте вашия идентификационен ключ и виртуални API ключове, които ви се отнасят и могат да достъпят {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Идентификационен ключ",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Вашият Личен API Ключ",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Вашият личен ключ за тази организация. Той е уникален за вашия акаунт и се използва за идентификация при обаждания на AI Gateway ресурси.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Вашият Личен API Ключ за {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Вашият личен ключ за тази организация. Използвайте го, за да извикате {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Свързани ключове",
|
||||||
|
"myVirtualApiKeysManualDescription": "Ръчни виртуални API ключове, свързани от администратор към вашия акаунт",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Ръчни виртуални API ключове, свързани с вашия акаунт, които могат да достъпят {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Все още няма приписани ключове",
|
||||||
|
"myVirtualApiKeysKindUser": "Идентичност",
|
||||||
|
"myVirtualApiKeysKindManual": "Ръчен",
|
||||||
|
"myVirtualApiKeysUnnamed": "Неназован ключ",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Разкриване на секрет",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Този секрет ви идентифицира за AI Gateway ресурси",
|
||||||
|
"aiProvidersTitle": "AI Доставчици",
|
||||||
|
"aiProvidersDescription": "Свързване на модели доставчици за AI задачи в тази организация",
|
||||||
|
"aiProvidersBannerTitle": "Свързване на модели доставчици",
|
||||||
|
"aiProvidersBannerDescription": "Доставчиците са бекендите на модела, които Pangolin използва за AI задачи. Свържете OpenAI, Anthropic и други доставчици тук, след това ги прикачете към публични AI шлюзове, така че потребителите да могат да извикват модели с идентичност-назнаван достъп, бюджети и логиране.",
|
||||||
|
"aiProvidersAdd": "Добавяне на Доставчик",
|
||||||
|
"aiProvidersSearch": "Търсене на доставчици...",
|
||||||
|
"aiProvidersEmpty": "Все още няма AI доставчици",
|
||||||
|
"aiProviderCreate": "Създаване на AI Доставчик",
|
||||||
|
"aiProviderCreateDescription": "Добавяне на модел доставчик за тази организация",
|
||||||
|
"aiProviderSeeAll": "Преглед на всички доставчици",
|
||||||
|
"aiProviderSetting": "Настройки на доставчика за {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Конфигурирайте този AI доставчик",
|
||||||
|
"aiProviderGeneral": "Общи",
|
||||||
|
"aiProviderGeneralDescription": "Основни настройки за този доставчик",
|
||||||
|
"aiProviderConfiguration": "Конфигурация",
|
||||||
|
"aiProviderConfigurationDescription": "Мрежов маршрутизация и удостоверяване за този доставчик",
|
||||||
|
"aiProviderNetworkSettings": "Мрежови настройки",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Избор как трафикът достига до този доставчик",
|
||||||
|
"aiProviderAuthSettings": "Удостоверяване",
|
||||||
|
"aiProviderAuthSettingsDescription": "Конфигурирайте как този доставчик удостоверява заявки до своя URL",
|
||||||
|
"aiProviderBudgetSettings": "Бюджет",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Конфигурирайте как този доставчик ограничава използването въз основа на разходи или жетонни лимити",
|
||||||
|
"aiBudgetAdd": "Добавяне на Бюджет",
|
||||||
|
"aiBudgetEmpty": "Все още няма конфигурирани бюджети. Щракнете върху Добавяне на Бюджет, за да настроите лимит на разходи или жетони.",
|
||||||
|
"aiBudgetUnit": "Вид на разхода",
|
||||||
|
"aiBudgetPeriod": "Период на нулиране",
|
||||||
|
"aiBudgetAmount": "Максимален разход",
|
||||||
|
"aiBudgetAmountPlaceholder": "Максимален разход",
|
||||||
|
"aiBudgetPeriodHourly": "Часов",
|
||||||
|
"aiBudgetPeriodDaily": "Дневен",
|
||||||
|
"aiBudgetPeriodWeekly": "Седмичен",
|
||||||
|
"aiBudgetPeriodMonthly": "Месечен",
|
||||||
|
"aiBudgetPeriodYearly": "Годишен",
|
||||||
|
"aiBudgetPeriodLifetime": "Пожизнен",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Жетони",
|
||||||
|
"aiBudgetConflictError": "Бюджет за този период на нулиране и вид на разхода вече съществува",
|
||||||
|
"aiBudgetInvalidAmountError": "Въведете максимален разход, по-голям от 0",
|
||||||
|
"aiBudgetUpdated": "Актуализирани бюджети",
|
||||||
|
"aiBudgetErrorSave": "Неуспех при актуализация на бюджети",
|
||||||
|
"aiProviderType": "Вид на доставчика",
|
||||||
|
"aiProviderTypeSearch": "Търсене на доставчици...",
|
||||||
|
"aiProviderTypeNotFound": "Не е намерен вид доставчик",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "Персонализиран",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Рунтайм API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "Използвайте собствена крайна точка или маршрутирайте чрез цели на сайта",
|
||||||
|
"aiProviderUpstreamUrl": "Възходящ URL адрес",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Основен URL за API на доставчика",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Оставете празно, за да използвате стандартния възходящ URL за този доставчик",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Ефективен възходящ URL адрес",
|
||||||
|
"aiProviderApiKey": "API ключ",
|
||||||
|
"aiProviderApiKeyDescription": "API ключ, използван за удостоверяване на заявки към този доставчик",
|
||||||
|
"aiProviderCustomHeadersDescription": "Заглавки, изпратени при всяка заявка към този доставчик. Отделени със нов ред: Име-на-заглавие: стойност",
|
||||||
|
"aiProviderApiKeyLastChars": "API ключ",
|
||||||
|
"aiProviderAuthType": "Тип удостоверяване",
|
||||||
|
"aiProviderAuthTypeSearch": "Търсене на типове удостоверяване...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Не е намерен тип удостоверяване",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Удостоверяване: Bearer ключ. Използва се от OpenAI и повечето доставчици",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key заглавие. Използва се от Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key заглавие. Използва се от Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Удостоверяване: Splunk ключ. Използва се от Splunk HTTP Event Collector",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer ключ. Използва се от Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "Без удостоверяване",
|
||||||
|
"aiProviderAuthTypePassthrough": "Премини",
|
||||||
|
"aiProviderAuthTypeDescription": "Как възходящото API удостоверява заявките",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Препрати заглавките на API ключа на повикващия към възходящото",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Не изпращайте заглавки за удостоверяване към възходящото",
|
||||||
|
"aiProviderRoutingMode": "Режим на маршрутиране",
|
||||||
|
"aiProviderRoutingModeDescription": "Изпрати трафик към възходящ URL или HTTP цели на вашите сайтове",
|
||||||
|
"aiProviderRoutingModeUrl": "Възходящ URL адрес",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Обадете се на публичен или частен API основен URL",
|
||||||
|
"aiProviderRoutingModeTarget": "Цели на сайта",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Маршрутиране чрез цели на вашите сайтове",
|
||||||
|
"aiProviderRoutingModeTargetNote": "След създаването на този доставчик, конфигурирайте целите на сайта в раздела Настройки на мрежата.",
|
||||||
|
"aiProviderTargetNoOne": "Този доставчик няма цели. Добавете цел за маршрутиране на заявки чрез вашите сайтове.",
|
||||||
|
"aiProviderSkipTlsVerification": "Пропуснете проверката на TLS",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Деактивирайте проверката на TLS сертификат за възходящото свързване",
|
||||||
|
"aiProviderBudget": "Бюджет",
|
||||||
|
"aiProviderBudgetDescription": "Факултативен лимит за разходи или маркер за този доставчик",
|
||||||
|
"aiProviderBudgetAmount": "Сума на бюджета",
|
||||||
|
"aiProviderBudgetUnit": "Единица на бюджета",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Токени",
|
||||||
|
"aiProviderEnabled": "Активиран",
|
||||||
|
"aiProviderEnabledDescription": "Напълно деактивирайте този доставчик за всички ресурси",
|
||||||
|
"aiProviderErrorCreate": "Неуспешно създаване на AI доставчик",
|
||||||
|
"aiProviderErrorUpdate": "Неуспешно актуализиране на AI доставчик",
|
||||||
|
"aiProviderErrorDelete": "Неуспешно изтриване на AI доставчик",
|
||||||
|
"aiProviderErrorLoad": "Неуспешно зареждане на AI доставчик",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Въведете валиден възходящ URL адрес",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Възходящ URL адрес е задължителен за този доставчик",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Необходим е тип за удостоверяване",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API ключът е задължителен",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Маршрутизацията на целите на сайта е налична само за персонализирани доставчици",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Изберете поне една възможност на API",
|
||||||
|
"aiProviderCapabilities": "Възможности на API",
|
||||||
|
"aiProviderCapabilitiesDescription": "Изберете, които API формати може да управлява този доставчик. Известни доставчици започват с препоръчани настройки по подразбиране.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Изберете кои формати на API този персонализиран доставчик може да обработва",
|
||||||
|
"aiProviderCapabilitiesSelect": "Изберете възможности",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Няма намерени възможности",
|
||||||
|
"aiProviderCapabilitiesSearch": "Търсене на възможности...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Чат Завършвания",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Поддържа /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI Отговори",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Поддържа /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic Съобщения",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Поддържа /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini Генериране на Съдържание",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Поддържа директния Gemini API",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Бедрок Модел Активирай",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Поддържа Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex Генериране на Съдържание",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Поддържа Vertex AI Gemini формат",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex Сурово Предсказване",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Поддържа Vertex AI rawPredict за Anthropic модели",
|
||||||
|
"aiCapabilityBedrockConverse": "Бедрок Разговор",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Поддържа Amazon Bedrock Converse API",
|
||||||
|
"aiProviderCreated": "AI доставчикът е създаден",
|
||||||
|
"aiProviderUpdated": "AI доставчикът е обновен",
|
||||||
|
"aiProviderDeleted": "AI доставчикът е изтрит",
|
||||||
|
"aiProviderDelete": "Изтриване на Доставчик",
|
||||||
|
"aiProviderDeleteConfirm": "Изтриване на Доставчик",
|
||||||
|
"aiProviderQuestionRemove": "Сигурни ли сте, че искате да изтриете този AI доставчик?",
|
||||||
|
"aiProviderMessageRemove": "Това ще изтрие постоянно доставчика и неговите модели и цели. Това не може да бъде отменено.",
|
||||||
|
"aiProviderErrorNoUpdate": "AI доставчикът не е наличен за обновяване",
|
||||||
|
"aiProviderModels": "Модели",
|
||||||
|
"aiProviderModelsDescription": "Определете позволителните и блокиращите списъци за този доставчик. Заявките трябва да съвпадат с позволителен запис и да не съвпадат с блокиращ запис.",
|
||||||
|
"aiProviderCreateModelsDescription": "Изберете кои модели този доставчик може да обслужва. Празен позволителен списък осъжда целия трафик. Можете да добавите или промените модели по-късно.",
|
||||||
|
"aiProviderModelsPlaceholder": "Търсене на модели или въведете персонален ключ",
|
||||||
|
"aiProviderModelsAllow": "Позволителен Списък",
|
||||||
|
"aiProviderModelsAllowDescription": "Модели, които могат да бъдат използвани чрез този доставчик. Празно означава отричане на всички.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Въведете ключ на модела",
|
||||||
|
"aiProviderModelsAllowEmpty": "Все още няма добавени модели.",
|
||||||
|
"aiProviderModelsBlock": "Блокиращ Списък",
|
||||||
|
"aiProviderModelsBlockDescription": "Модели за отричане, дори ако съвпадат с позволителен запис.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Въведете ключ на модела",
|
||||||
|
"aiProviderModelsBlockEmpty": "Все още няма добавени модели.",
|
||||||
|
"aiProviderModelsAdd": "Добавяне на Модели",
|
||||||
|
"aiProviderModelsClearAll": "Изчистване на Всички",
|
||||||
|
"aiProviderModelsAddCustom": "Добавяне \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Натиснете Enter, за да добавите този персонален ключ на модел.",
|
||||||
|
"aiProviderModelsAddBulk": "Добавяне на {count} персонални ключове",
|
||||||
|
"aiProviderModelsAddBulkHint": "Натиснете Enter, за да добавите {count} персонални ключове.",
|
||||||
|
"aiProviderModelsAddOne": "Добавяне {key} сега",
|
||||||
|
"aiProviderModelsAddSelected": "Добавяне на Избраните",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} selected",
|
||||||
|
"aiProviderModelsSelectAll": "Избери всички",
|
||||||
|
"aiProviderModelsClearSelected": "Изчистване",
|
||||||
|
"aiProviderModelsBulkHint": "Изберете известни модели или въведете персонален ключ.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Няма съвпадение с каталога на модели.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Известни Модели",
|
||||||
|
"aiProviderModelsAllLabel": "Всички модели",
|
||||||
|
"aiProviderModelsAllPatternHint": "Joker: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Позволете всички модели",
|
||||||
|
"aiProviderModelsAddAllBlock": "Блокирайте всички модели",
|
||||||
|
"aiProviderModelsAddAllDescription": "Използва * joker, така че всеки ключ на модел съвпада.",
|
||||||
|
"aiProviderModelsViewMore": "Вижте още ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Вижте по-малко",
|
||||||
|
"aiProviderModelsRemove": "Премахване на модел",
|
||||||
|
"aiProviderModelsEditHint": "Натиснете, за да редактирате настройките на модела",
|
||||||
|
"aiProviderModelsSourceCatalog": "Известен каталогов модел",
|
||||||
|
"aiProviderModelsSourceCustom": "Персонализиран ключ на модел",
|
||||||
|
"aiProviderModelsSourcePattern": "Модел за съвпадение",
|
||||||
|
"aiProviderModelsSourceAll": "Съвпада с всеки ключ на модел",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Конфигуриран бюджет",
|
||||||
|
"aiProviderModelsEditTitle": "Редактиране на Модел",
|
||||||
|
"aiProviderModelsEditDescription": "Обновете ключа на модела или конфигурирайте бюджета му.",
|
||||||
|
"aiProviderModelsBudgetTab": "Бюджет",
|
||||||
|
"aiProviderModelsKeyLabel": "Ключ на модел",
|
||||||
|
"aiProviderModelsKeyRequired": "Въведете ключ на модел",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Този ключ на модел вече е в списък",
|
||||||
|
"aiProviderModelsOverlapError": "Тези образци не могат да бъдат в двата списъка: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Моделите са обновени",
|
||||||
|
"aiProviderModelsErrorUpdate": "Неуспешно обновяване на модели",
|
||||||
|
"aiResourceProviders": "Доставчици",
|
||||||
|
"aiResourceProvidersDescription": "Изберете кои AI доставчици този AI портал може да използва",
|
||||||
|
"aiResourceProvidersHelp": "Добавете доставчици и изберете наследяване (използване на списъците на всеки доставчик) или избор (изберете разрешителен списък за този ресурс). Разрешителните образци, които се конфликтират между прикрепени доставчици, не са разрешени.",
|
||||||
|
"aiResourceProvidersSelect": "Изберете доставчици",
|
||||||
|
"aiResourceProvidersEmpty": "Не са намерени AI доставчици",
|
||||||
|
"aiResourceProvidersNoneAttached": "Все още няма прикрепени доставчици.",
|
||||||
|
"aiResourceProvidersAdd": "Добавяне на доставчик",
|
||||||
|
"aiResourceProvidersRemove": "Премахване на доставчик",
|
||||||
|
"aiResourceProviderToggleEnabled": "Разрешете или забранете този доставчик на ресурса",
|
||||||
|
"aiResourceProviderDisabled": "Деактивиран",
|
||||||
|
"aiResourceProvidersUpdated": "Доставчиците са обновени",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Неуспешно обновяване на доставчици",
|
||||||
|
"aiResourceProviderEditDescription": "Изберете как да се изложат моделите на този доставчик върху този ресурс.",
|
||||||
|
"viewProviderSettings": "Преглед на настройки на доставчик",
|
||||||
|
"aiResourceProviderMode": "Режим на достъп",
|
||||||
|
"aiResourceProviderModeInherit": "Наследяване",
|
||||||
|
"aiResourceProviderModeSelect": "Изберете",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Изберете · {count} модели",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Използвайте разрешителните и блокиращите списъци на този доставчик според конфигурацията.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Изберете подмножество на моделите с разрешителния списък на този доставчик за този ресурс.",
|
||||||
|
"aiResourceProviderAllowModels": "Разрешителен списък",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Изберете модели",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Търсене на модели...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Няма намерени модели",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Само модели от разрешителния списък на този доставчик могат да бъдат избрани.",
|
||||||
|
"aiResourceAliasRequired": "Псевдоним е необходим за AI портали",
|
||||||
|
"aiResourceDomainConfiguration": "Конфигурация на Домейн",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Изберете домейна, който клиентите ще използват, за да достигнат до този AI портал.",
|
||||||
|
"aiUsageAnalyticsTitle": "Аналитика на AI Използването",
|
||||||
|
"aiUsageAnalyticsDescription": "Анализирайте разходите, употребата на токени и активността на AI портал през доставчици, ресурси, роли и потребители",
|
||||||
|
"aiUsageTabOverview": "Общ поглед",
|
||||||
|
"aiUsageTabProviders": "Доставчици",
|
||||||
|
"aiUsageTabResources": "Ресурси",
|
||||||
|
"aiUsageFilterProvider": "Доставчик",
|
||||||
|
"aiUsageFilterModel": "Модел",
|
||||||
|
"aiUsageFilterResource": "Ресурс",
|
||||||
|
"aiUsageFilterRole": "Роля",
|
||||||
|
"aiUsageFilterUser": "Потребител",
|
||||||
|
"aiUsageFilterAllProviders": "Всички Доставчици",
|
||||||
|
"aiUsageFilterAllModels": "Всички Модели",
|
||||||
|
"aiUsageFilterAllResources": "Всички Ресурси",
|
||||||
|
"aiUsageFilterAllRoles": "Всички Роли",
|
||||||
|
"aiUsageFilterAllUsers": "Всички Потребители",
|
||||||
|
"aiUsageFilterSearch": "Търсене...",
|
||||||
|
"aiUsageFilterNotFound": "Не са намерени опции",
|
||||||
|
"aiUsageResetFilters": "Възстановяване на Филтрите",
|
||||||
|
"aiUsageRefresh": "Обновяване",
|
||||||
|
"aiUsageTokenTypePrompt": "Подканя",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Четене от кеш",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Писане в кеш",
|
||||||
|
"aiUsageTokenTypeCompletion": "Завършване",
|
||||||
|
"aiUsageTokenTypeReasoning": "Разсъждение",
|
||||||
|
"aiUsageRequests": "Запитвания",
|
||||||
|
"aiUsageCost": "Разход",
|
||||||
|
"aiUsageTokens": "Токени",
|
||||||
|
"aiUsageOther": "Друго",
|
||||||
|
"aiUsageTotalRequests": "Общо Запитвания",
|
||||||
|
"aiUsageTotalTokens": "Общо Токени",
|
||||||
|
"aiUsageTotalCost": "Общ Разход",
|
||||||
|
"aiUsageEstimated": "Оценени",
|
||||||
|
"aiUsageRequestVolume": "Обем на Запитване",
|
||||||
|
"aiUsageTokenUsage": "Употреба на Токени",
|
||||||
|
"aiUsageModelCost": "Модел Разход",
|
||||||
|
"aiUsageModelTokens": "Модел Токени",
|
||||||
|
"aiUsageTopModels": "Топ Модели",
|
||||||
|
"aiUsageTopProviders": "Топ Доставчици",
|
||||||
|
"aiUsageProviderCost": "Разход на Доставчик",
|
||||||
|
"aiUsageProviderTokenUsage": "Употреба на Доставчик на Токени",
|
||||||
|
"aiUsageTopResources": "Топ Ресурси",
|
||||||
|
"aiUsageResourceCost": "Разход на Ресурс",
|
||||||
|
"aiUsageResourceTokenUsage": "Употреба на Токени на Ресурс",
|
||||||
|
"aiUsageResourceTypePublic": "Публичен Ресурс",
|
||||||
|
"aiUsageResourceTypeSite": "Частен Ресурс",
|
||||||
|
"aiUsageNoResource": "Няма ресурс",
|
||||||
|
"aiUsageRolesTab": "Роли",
|
||||||
|
"aiUsageUsersTab": "Потребители",
|
||||||
|
"aiUsageTopRoles": "Топ Роли",
|
||||||
|
"aiUsageRoleCost": "Разход на Роля",
|
||||||
|
"aiUsageRoleTokenUsage": "Употреба на Токени на Роля",
|
||||||
|
"aiUsageTopUsers": "Топ Потребители",
|
||||||
|
"aiUsageUserCost": "Разход на Потребител",
|
||||||
|
"aiUsageUserTokenUsage": "Употреба на Токени на Потребител",
|
||||||
|
"aiUsageUnknownUser": "Непознат потребител",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Виртуални API Ключове",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Виртуален API Ключ",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Всички Виртуални API Ключове",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Топ Виртуални API Ключове",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Разход на Виртуален API Ключ",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Употреба на Токени на Виртуален API Ключ",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Няма виртуален API ключ",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Неназован ключ",
|
||||||
|
"aiUsageLoading": "Зареждане...",
|
||||||
|
"aiUsageNoData": "Няма данни",
|
||||||
|
"resourceBudgetSettings": "Бюджет",
|
||||||
|
"resourceBudgetSettingsDescription": "Конфигурирайте как AI порталът ограничава употребата, основавайки се на разходи или лимити на токени",
|
||||||
"sidebarApiKeys": "API ключове",
|
"sidebarApiKeys": "API ключове",
|
||||||
"sidebarProvisioning": "Осигуряване",
|
"sidebarProvisioning": "Осигуряване",
|
||||||
"sidebarSettings": "Настройки",
|
"sidebarSettings": "Настройки",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Мрежа",
|
"subnetPlaceholder": "Мрежа",
|
||||||
"addressDescription": "Вътрешният адрес на клиента. Трябва да пада в подмрежата на организацията.",
|
"addressDescription": "Вътрешният адрес на клиента. Трябва да пада в подмрежата на организацията.",
|
||||||
"selectSites": "Избор на сайтове",
|
"selectSites": "Избор на сайтове",
|
||||||
|
"selectResources": "Изберете ресурси",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# ресурс} other {# ресурси}}",
|
||||||
"selectLabels": "Изберете етикети",
|
"selectLabels": "Изберете етикети",
|
||||||
"sitesDescription": "Клиентът ще има връзка с избраните сайтове",
|
"sitesDescription": "Клиентът ще има връзка с избраните сайтове",
|
||||||
"clientInstallOlm": "Инсталиране на Olm",
|
"clientInstallOlm": "Инсталиране на Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Времето е в секунди",
|
"timeIsInSeconds": "Времето е в секунди",
|
||||||
"requireDeviceApproval": "Изискват одобрение на устройства",
|
"requireDeviceApproval": "Изискват одобрение на устройства",
|
||||||
"requireDeviceApprovalDescription": "Потребители с тази роля трябва да имат нови устройства одобрени от администратор преди да могат да се свържат и да имат достъп до ресурси.",
|
"requireDeviceApprovalDescription": "Потребители с тази роля трябва да имат нови устройства одобрени от администратор преди да могат да се свържат и да имат достъп до ресурси.",
|
||||||
"sshSettings": "Настройки за SSH",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI Портал",
|
||||||
"sshAccess": "SSH Достъп",
|
"sshAccess": "SSH Достъп",
|
||||||
"rdpSettings": "Настройки за RDP",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "Настройки за VNC",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH сървър",
|
"sshServer": "SSH сървър",
|
||||||
"rdpServer": "RDP сървър",
|
"rdpServer": "RDP сървър",
|
||||||
"vncServer": "VNC сървър",
|
"vncServer": "VNC сървър",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI Портал",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Метод",
|
"editInternalResourceDialogScheme": "Метод",
|
||||||
"editInternalResourceDialogEnableSsl": "Активиране на TLS",
|
"editInternalResourceDialogEnableSsl": "Активиране на TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Разрешете ICMP Ping",
|
"privateResourceAllowIcmpPing": "Разрешете ICMP Ping",
|
||||||
"privateResourceNetworkAccess": "Достъп до мрежата",
|
"privateResourceNetworkAccess": "Достъп до мрежата",
|
||||||
"privateResourceNetworkAccessDescription": "Контролирайте достъпа до TCP/UDP портовете и дали ICMP пинг е разрешен за този ресурс.",
|
"privateResourceNetworkAccessDescription": "Контролирайте достъпа до TCP/UDP портовете и дали ICMP пинг е разрешен за този ресурс.",
|
||||||
"hostSettings": "Настройки на хоста",
|
"hostSettings": "Хост",
|
||||||
"cidrSettings": "Настройки на CIDR",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Свойства на ресурса",
|
"createInternalResourceDialogResourceProperties": "Свойства на ресурса",
|
||||||
"createInternalResourceDialogName": "Име",
|
"createInternalResourceDialogName": "Име",
|
||||||
"createInternalResourceDialogSite": "Сайт",
|
"createInternalResourceDialogSite": "Сайт",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI Портал",
|
||||||
"scheme": "Метод",
|
"scheme": "Метод",
|
||||||
"createInternalResourceDialogScheme": "Метод",
|
"createInternalResourceDialogScheme": "Метод",
|
||||||
"createInternalResourceDialogEnableSsl": "Активиране на TLS",
|
"createInternalResourceDialogEnableSsl": "Активиране на TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Валидна парола",
|
"validPassword": "Валидна парола",
|
||||||
"validEmail": "Валиден имейл",
|
"validEmail": "Валиден имейл",
|
||||||
"validSSO": "Валидно SSO",
|
"validSSO": "Валидно SSO",
|
||||||
|
"validVirtualAPIKey": "Валиден Виртуален API Ключ",
|
||||||
"view": "Преглед",
|
"view": "Преглед",
|
||||||
"configManaged": "Управлявана конфигурация",
|
"configManaged": "Управлявана конфигурация",
|
||||||
"connectedClient": "Свързан клиент",
|
"connectedClient": "Свързан клиент",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Задържане на логове",
|
"logRetention": "Задържане на логове",
|
||||||
"logRetentionDescription": "Управлявайте времето за задържане на различни видове логове за тази организация или ги деактивирайте",
|
"logRetentionDescription": "Управлявайте времето за задържане на различни видове логове за тази организация или ги деактивирайте",
|
||||||
"requestLogsDescription": "Прегледайте подробни логове на заявки за ресурси в тази организация",
|
"requestLogsDescription": "Прегледайте подробни логове на заявки за ресурси в тази организация",
|
||||||
|
"aiSessionLogs": "Журнали на AI Портал Сесиите",
|
||||||
|
"aiSessionLogsDescription": "Прегледайте подканянета и транскрипции на отговори за запитванията към AI портал в тази организация",
|
||||||
|
"sidebarLogsAi": "Журнали на Сесиите",
|
||||||
|
"commandLogsAi": "Журнали на Сесиите",
|
||||||
|
"sidebarLogsAiUsage": "Анализ на Използване",
|
||||||
|
"commandLogsAiUsage": "Анализ на Използване",
|
||||||
|
"provider": "Доставчик",
|
||||||
|
"capability": "Способност",
|
||||||
|
"model": "Модел",
|
||||||
|
"virtualApiKey": "Виртуален API Ключ",
|
||||||
|
"noVirtualApiKey": "Няма виртуален API ключ",
|
||||||
|
"stream": "Поток",
|
||||||
|
"streaming": "Стрийминг",
|
||||||
|
"nonStreaming": "Нестрийминг",
|
||||||
|
"statusCode": "Код на Статуса",
|
||||||
|
"aiSessionId": "ID на Сесията",
|
||||||
|
"aiSessionRequest": "Запитване",
|
||||||
|
"aiSessionResponse": "Отговор",
|
||||||
|
"aiSessionNoData": "Няма събрани данни",
|
||||||
|
"aiSessionCouldNotParse": "(суров, не може да се парсира транскрипт)",
|
||||||
|
"aiSessionLogTruncated": "Тази сесия беше прекъсната преди съхранение и може да е непълна.",
|
||||||
|
"aiSessionViewRaw": "Преглед на Суров JSON",
|
||||||
|
"aiSessionViewChat": "Преглед на Чат",
|
||||||
|
"cost": "Разход",
|
||||||
|
"estimated": "Оценени",
|
||||||
|
"tokenUsage": "Употреба на Токени",
|
||||||
|
"promptTokens": "Подканящи Токени",
|
||||||
|
"cacheReadTokens": "Кеш Прочитане на Токени",
|
||||||
|
"cacheWriteTokens": "Кеш Писане на Токени",
|
||||||
|
"completionTokens": "Токени за Завършване",
|
||||||
|
"reasoningTokens": "Токени за Разсъждение",
|
||||||
|
"totalTokens": "Общо Токени",
|
||||||
"requestAnalyticsDescription": "Вижте подробни анализи на заявки за ресурсите в тази организация",
|
"requestAnalyticsDescription": "Вижте подробни анализи на заявки за ресурсите в тази организация",
|
||||||
"logRetentionRequestLabel": "Задържане на логове за HTTP заявки",
|
"logRetentionRequestLabel": "Задържане на логове за HTTP заявки",
|
||||||
"logRetentionRequestDescription": "Колко дълго да се задържат логовете на заявките",
|
"logRetentionRequestDescription": "Колко дълго да се задържат логовете на заявките",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Колко дълго да се задържат логовете за действия",
|
"logRetentionActionDescription": "Колко дълго да се задържат логовете за действия",
|
||||||
"logRetentionConnectionLabel": "Запазване на дневниците на връзките",
|
"logRetentionConnectionLabel": "Запазване на дневниците на връзките",
|
||||||
"logRetentionConnectionDescription": "Колко дълго да се съхраняват дневниците на връзките",
|
"logRetentionConnectionDescription": "Колко дълго да се съхраняват дневниците на връзките",
|
||||||
|
"logRetentionAISessionsLabel": "Съхранение на Журнали на AI Портал Сесиите",
|
||||||
|
"logRetentionAISessionsDescription": "Колко време да съхраняваме подканящите/отговоряващите журнали на AI портал сесии",
|
||||||
"logRetentionDisabled": "Деактивирано",
|
"logRetentionDisabled": "Деактивирано",
|
||||||
"logRetention3Days": "3 дни",
|
"logRetention3Days": "3 дни",
|
||||||
"logRetention7Days": "7 дни",
|
"logRetention7Days": "7 дни",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Източен адрес",
|
"sourceAddress": "Източен адрес",
|
||||||
"destinationAddress": "Адрес на дестинация",
|
"destinationAddress": "Адрес на дестинация",
|
||||||
"duration": "Продължителност",
|
"duration": "Продължителност",
|
||||||
"licenseRequiredToUse": "Изисква се лиценз за <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>, за да използвате тази функция. <bookADemoLink>Резервирайте безплатна демонстрация или пробен POC, за да научите повече.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Ентерпрайз Едишън лиценз</enterpriseEditionLink> е необходим. <bookADemoLink>Резервирайте демо или проба</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> е необходим за използване на тази функция. Тази функция също е налична в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Резервирайте безплатна демонстрация или пробен POC, за да научите повече.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Ентерпрайз Едишън лиценз</enterpriseEditionLink> е необходим. <bookADemoLink>Резервирайте демо или проба</bookADemoLink>",
|
||||||
"certResolver": "Решавач на сертификати",
|
"certResolver": "Решавач на сертификати",
|
||||||
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
|
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
|
||||||
"selectCertResolver": "Изберете решавач на сертификати",
|
"selectCertResolver": "Изберете решавач на сертификати",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Избирайки няколко сайта, се осигурява сигурен път и пренасочване при висока достъпност.",
|
"internalResourceFormMultiSiteRoutingHelp": "Избирайки няколко сайта, се осигурява сигурен път и пренасочване при висока достъпност.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Научете повече",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Научете повече",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Ограничете достъпа до конкретни TCP/UDP портове или позволете/блокирайте всички портове.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Ограничете достъпа до конкретни TCP/UDP портове или позволете/блокирайте всички портове.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "Конфигурация HTTP",
|
"createInternalResourceDialogHttpConfiguration": "Конфигурация на Домейн",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Изберете домейна, който клиентите ще използват, за да достигнат този ресурс чрез HTTP или HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Изберете домейна, който клиентите ще използват, за да достигнат този ресурс чрез HTTP или HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "Конфигурация HTTP",
|
"editInternalResourceDialogHttpConfiguration": "Конфигурация на Домейн",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Изберете домейна, който клиентите ще използват, за да достигнат този ресурс чрез HTTP или HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Изберете домейна, който клиентите ще използват, за да достигнат този ресурс чрез HTTP или HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP.",
|
"editInternalResourceDialogTcp": "TCP.",
|
||||||
"editInternalResourceDialogUdp": "UDP.",
|
"editInternalResourceDialogUdp": "UDP.",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Без Етикет",
|
"resourceLauncherUnlabeled": "Без Етикет",
|
||||||
|
"resourceLauncherAiGateway": "AI Портал",
|
||||||
"resourceLauncherNoSite": "Няма Сайт",
|
"resourceLauncherNoSite": "Няма Сайт",
|
||||||
|
"resourceLauncherAvailableModels": "Налични Модели",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Модели, които можете да използвате с този AI портал.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Няма налични модели за този ресурс.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Не можа да се заредят наличните модели.",
|
||||||
|
"resourceLauncherApiKeys": "API Ключове",
|
||||||
|
"resourceLauncherApiKeysDescription": "Използвайте вашия ключ за идентичност или атрибутиран ключ за удостоверяване с този ресурс.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Ключ за Идентичност",
|
||||||
|
"resourceLauncherApiKeysManual": "Атрибуте Ключове",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Няма налични API ключове за този ресурс.",
|
||||||
|
"resourceLauncherApiKeysError": "Не можа да се заредят API ключовете.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Няма ресурси в тази група",
|
"resourceLauncherNoResourcesInGroup": "Няма ресурси в тази група",
|
||||||
"resourceLauncherEmptyStateTitle": "Няма Налични Ресурси",
|
"resourceLauncherEmptyStateTitle": "Няма Налични Ресурси",
|
||||||
"resourceLauncherEmptyStateDescription": "Все още нямате достъп до никакви ресурси. Свържете се с вашия администратор, за да поискате достъп.",
|
"resourceLauncherEmptyStateDescription": "Все още нямате достъп до никакви ресурси. Свържете се с вашия администратор, за да поискате достъп.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Не можахте да изтриете изгледа на стартер. Моля, опитайте отново.",
|
"resourceLauncherViewDeleteFailedDescription": "Не можахте да изтриете изгледа на стартер. Моля, опитайте отново.",
|
||||||
"memberPortalPrevious": "Предишен",
|
"memberPortalPrevious": "Предишен",
|
||||||
"memberPortalNext": "Следващ",
|
"memberPortalNext": "Следващ",
|
||||||
"httpSettings": "HTTP настройки",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "TCP настройки",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "UDP настройки",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Установяване на защитена връзка…",
|
"sshConnectingDescription": "Установяване на защитена връзка…",
|
||||||
"sshConnecting": "Свързване…",
|
"sshConnecting": "Свързване…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Prosím ujistěte se, že jste přihlášeni jako správný uživatel.",
|
"inviteLoginUser": "Prosím ujistěte se, že jste přihlášeni jako správný uživatel.",
|
||||||
"inviteErrorNoUser": "Je nám líto, ale vypadá to, že pozvánka, ke které se snažíte získat přístup, není pro existujícího uživatele.",
|
"inviteErrorNoUser": "Je nám líto, ale vypadá to, že pozvánka, ke které se snažíte získat přístup, není pro existujícího uživatele.",
|
||||||
"inviteCreateUser": "Nejprve si prosím vytvořte účet.",
|
"inviteCreateUser": "Nejprve si prosím vytvořte účet.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Pozvánky mohou být přijaty pouze vnitřními účty. Odhlaste se a přihlaste se pomocí hesla k tomuto e-mailu.",
|
||||||
|
"inviteLoginInternalOnly": "Pozvánky vyžadují vnitřní účet s heslem. Vytvořte si účet nebo se přihlaste pomocí hesla.",
|
||||||
"goHome": "Přejít na hlavní stránku",
|
"goHome": "Přejít na hlavní stránku",
|
||||||
"inviteLogInOtherUser": "Přihlásit se jako jiný uživatel",
|
"inviteLogInOtherUser": "Přihlásit se jako jiný uživatel",
|
||||||
"createAnAccount": "Vytvořit účet",
|
"createAnAccount": "Vytvořit účet",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Nebyla nalezena žádná země.",
|
"noCountryFound": "Nebyla nalezena žádná země.",
|
||||||
"siteSelectionDescription": "Tato lokalita poskytne připojení k cíli.",
|
"siteSelectionDescription": "Tato lokalita poskytne připojení k cíli.",
|
||||||
"resourceType": "Typ zdroje",
|
"resourceType": "Typ zdroje",
|
||||||
"resourceTypeDescription": "Toto určuje protokol zdroje a jak bude zobrazen v prohlížeči. Později to nelze změnit.",
|
"resourceTypeDescription": "Toto řídí protokol zdroje a nelze to později změnit.",
|
||||||
|
"resourceTypeSearch": "Hledat typy zdrojů...",
|
||||||
|
"resourceTypeNotFound": "Typ zdroje nenalezen",
|
||||||
|
"resourceTypeHttpDescription": "Proxy webový provoz přes HTTPS pomocí názvu domény",
|
||||||
|
"resourceTypeInferenceDescription": "Směřujte žádosti o AI přes připojené poskytovatele",
|
||||||
|
"resourceTypeSshDescription": "Přístup k SSH serveru z prohlížeče",
|
||||||
|
"resourceTypeRdpDescription": "Přístup k vzdálené ploše z prohlížeče",
|
||||||
|
"resourceTypeVncDescription": "Přístup k VNC ploše z prohlížeče",
|
||||||
|
"resourceTypeTcpDescription": "Proxy surový TCP provoz pomocí čísla portu",
|
||||||
|
"resourceTypeUdpDescription": "Proxy hrubý UDP provoz pomocí čísla portu",
|
||||||
|
"privateResourceTypeDescription": "Toto řídí, jak se klienti dostávají ke zdroji. Toto nelze později změnit.",
|
||||||
|
"privateResourceTypeHostDescription": "Vystavit jeden hostitel na síti stránky propojeným klientům",
|
||||||
|
"privateResourceTypeCidrDescription": "Vystavit CIDR rozsah na síti stránky propojeným klientům",
|
||||||
|
"privateResourceTypeHttpDescription": "Přístup ke službě HTTP nebo HTTPS pomocí domény",
|
||||||
|
"privateResourceTypeSshDescription": "Přístup k SSH serveru připojenými klienty",
|
||||||
"resourceDomainDescription": "Zdroji bude obsluhován tento plně kvalifikovaný doménový název.",
|
"resourceDomainDescription": "Zdroji bude obsluhován tento plně kvalifikovaný doménový název.",
|
||||||
"resourceHTTPSSettings": "Nastavení HTTPS",
|
"resourceHTTPSSettings": "Nastavení HTTPS",
|
||||||
"resourceHTTPSSettingsDescription": "Nakonfigurujte, jak bude dokument přístupný přes HTTPS",
|
"resourceHTTPSSettingsDescription": "Nakonfigurujte, jak bude dokument přístupný přes HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Uložit nastavení",
|
"authMethodsSave": "Uložit nastavení",
|
||||||
"policyAuthStackTitle": "Autentifikace",
|
"policyAuthStackTitle": "Autentifikace",
|
||||||
"policyAuthStackDescription": "Určete, které metody autentifikace jsou požadovány pro přístup k tomuto zdroji",
|
"policyAuthStackDescription": "Určete, které metody autentifikace jsou požadovány pro přístup k tomuto zdroji",
|
||||||
|
"policyAuthInferenceStackDescription": "Zvolte, kteří uživatelé a role se mohou autentifikovat k této AI bráně",
|
||||||
"policyAuthOrLogicTitle": "Více metod autentifikace je aktivních",
|
"policyAuthOrLogicTitle": "Více metod autentifikace je aktivních",
|
||||||
"policyAuthOrLogicBanner": "Návštěvníci mohou použít jakoukoli aktivní metodu uvedenou níže. Nemusí splnit všechny z nich.",
|
"policyAuthOrLogicBanner": "Návštěvníci mohou použít jakoukoli aktivní metodu uvedenou níže. Nemusí splnit všechny z nich.",
|
||||||
"policyAuthMethodActive": "Aktivní",
|
"policyAuthMethodActive": "Aktivní",
|
||||||
"policyAuthMethodOff": "Vypnuto",
|
"policyAuthMethodOff": "Vypnuto",
|
||||||
"policyAuthSsoTitle": "Platformové SSO",
|
"policyAuthSsoTitle": "Platformové SSO",
|
||||||
"policyAuthSsoDescription": "Požadujte přihlášení prostřednictvím identifikačního poskytovatele vaší organizace",
|
"policyAuthSsoDescription": "Požadujte přihlášení prostřednictvím identifikačního poskytovatele vaší organizace",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "Přihlašovací URL",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Každý uživatel již má identitu API klíč, takže stačí vytvořit virtuální API klíče pro ne-uživatelské klienty nebo sdílený přístup. Uživatelé mohou svůj klíč získat přihlášením pomocí svého poskytovatele identity na URL této zdroje, kde bude zobrazen po přihlášení.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Každý uživatel již má identitu API klíč, takže stačí vytvořit virtuální API klíče pro ne-uživatelské klienty nebo sdílený přístup. Uživatelé získávají svůj klíč zde po přihlášení s poskytovatelem identity.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} uživatelé, {roles} role",
|
"policyAuthSsoSummary": "{idp} · {users} uživatelé, {roles} role",
|
||||||
"policyAuthSsoDefaultIdp": "Výchozí poskytovatel",
|
"policyAuthSsoDefaultIdp": "Výchozí poskytovatel",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Přidat výchozího identifikačního poskytovatele",
|
"policyAuthAddDefaultIdentityProvider": "Přidat výchozího identifikačního poskytovatele",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Je vyžadována nová role",
|
"accessRoleErrorNewRequired": "Je vyžadována nová role",
|
||||||
"accessRoleErrorRemove": "Nepodařilo se odstranit roli",
|
"accessRoleErrorRemove": "Nepodařilo se odstranit roli",
|
||||||
"accessRoleErrorRemoveDescription": "Došlo k chybě při odstraňování role.",
|
"accessRoleErrorRemoveDescription": "Došlo k chybě při odstraňování role.",
|
||||||
|
"accessRoleInferenceBudget": "AI Rozpočet",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Konfigurace, jak se členové této role omezují AI spotřebu podle výdajů nebo limitů tokenů",
|
||||||
"accessRoleName": "Název role",
|
"accessRoleName": "Název role",
|
||||||
"accessRoleQuestionRemove": "Chystáte se odstranit roli `{name}`. Tuto akci nelze vrátit zpět.",
|
"accessRoleQuestionRemove": "Chystáte se odstranit roli `{name}`. Tuto akci nelze vrátit zpět.",
|
||||||
"accessRoleRemove": "Odstranit roli",
|
"accessRoleRemove": "Odstranit roli",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Odstranění lokality",
|
"actionDeleteSite": "Odstranění lokality",
|
||||||
"actionGetSite": "Získat web",
|
"actionGetSite": "Získat web",
|
||||||
"actionListSites": "Seznam stránek",
|
"actionListSites": "Seznam stránek",
|
||||||
|
"actionCreateAiProvider": "Vytvořit poskytovatele AI",
|
||||||
|
"actionDeleteAiProvider": "Smazat poskytovatele AI",
|
||||||
|
"actionGetAiProvider": "Získat poskytovatele AI",
|
||||||
|
"actionListAiProviders": "Seznam poskytovatelů AI",
|
||||||
|
"actionUpdateAiProvider": "Aktualizovat poskytovatele AI",
|
||||||
|
"actionCreateAiModel": "Vytvořit AI model",
|
||||||
|
"actionDeleteAiModel": "Smazat AI model",
|
||||||
|
"actionGetAiModel": "Získat AI model",
|
||||||
|
"actionListAiModels": "Seznam AI modelů",
|
||||||
|
"actionUpdateAiModel": "Aktualizovat AI model",
|
||||||
|
"actionCreateAiBudget": "Vytvořit AI rozpočet",
|
||||||
|
"actionDeleteAiBudget": "Smazat AI rozpočet",
|
||||||
|
"actionGetAiBudget": "Získat AI rozpočet",
|
||||||
|
"actionListAiBudgets": "Seznam AI rozpočtů",
|
||||||
|
"actionUpdateAiBudget": "Aktualizovat AI rozpočet",
|
||||||
|
"actionListResourceAiModels": "Seznam AI modelů zdroje",
|
||||||
|
"actionSetResourceAiModels": "Nastavit AI modely zdroje",
|
||||||
|
"actionCreateVirtualApiKey": "Vytvořit virtuální API klíč",
|
||||||
|
"actionDeleteVirtualApiKey": "Smazat virtuální API klíč",
|
||||||
|
"actionGetVirtualApiKey": "Získat virtuální API klíč",
|
||||||
|
"actionListVirtualApiKeys": "Seznam virtuálních API klíčů",
|
||||||
|
"actionUpdateVirtualApiKey": "Aktualizovat virtuální API klíč",
|
||||||
"actionApplyBlueprint": "Použít plán",
|
"actionApplyBlueprint": "Použít plán",
|
||||||
"actionListBlueprints": "Seznam šablon",
|
"actionListBlueprints": "Seznam šablon",
|
||||||
"actionGetBlueprint": "Získat šablonu",
|
"actionGetBlueprint": "Získat šablonu",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Nastavit zdrojový kód",
|
"actionSetResourcePincode": "Nastavit zdrojový kód",
|
||||||
"actionSetResourceEmailWhitelist": "Nastavit seznam povolených dokumentů",
|
"actionSetResourceEmailWhitelist": "Nastavit seznam povolených dokumentů",
|
||||||
"actionGetResourceEmailWhitelist": "Získat seznam povolených dokumentů",
|
"actionGetResourceEmailWhitelist": "Získat seznam povolených dokumentů",
|
||||||
|
"actionListResourcePolicies": "Seznam zásad zdrojů",
|
||||||
|
"actionCreateResourcePolicy": "Vytvořit zásadu zdrojů",
|
||||||
"actionGetResourcePolicy": "Získat zásady zdroje",
|
"actionGetResourcePolicy": "Získat zásady zdroje",
|
||||||
"actionUpdateResourcePolicy": "Aktualizovat zásady zdroje",
|
"actionUpdateResourcePolicy": "Aktualizovat zásady zdroje",
|
||||||
|
"actionDeleteResourcePolicy": "Smazat zásadu zdrojů",
|
||||||
"actionSetResourcePolicyUsers": "Nastavte uživatele pro zásady zdroje",
|
"actionSetResourcePolicyUsers": "Nastavte uživatele pro zásady zdroje",
|
||||||
"actionSetResourcePolicyRoles": "Nastavte role pro zásady zdroje",
|
"actionSetResourcePolicyRoles": "Nastavte role pro zásady zdroje",
|
||||||
"actionSetResourcePolicyPassword": "Nastavte heslo pro zásady zdroje",
|
"actionSetResourcePolicyPassword": "Nastavte heslo pro zásady zdroje",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Pozvánky",
|
"sidebarInvitations": "Pozvánky",
|
||||||
"sidebarRoles": "Role",
|
"sidebarRoles": "Role",
|
||||||
"sidebarShareableLinks": "Sdílené Odkazy",
|
"sidebarShareableLinks": "Sdílené Odkazy",
|
||||||
|
"sidebarAiGateway": "AI Brána",
|
||||||
|
"sidebarAiProviders": "Poskytovatelé",
|
||||||
|
"commandAiProviders": "AI Poskytovatelé",
|
||||||
|
"sidebarVirtualApiKeys": "Virtuální API Klíče",
|
||||||
|
"sidebarMyApiKeys": "Vaše API Klíče",
|
||||||
|
"sidebarAccount": "Spouštěč",
|
||||||
|
"commandVirtualApiKeys": "Virtuální API Klíče",
|
||||||
|
"virtualApiKeysTitle": "Spravovat virtuální API Klíče",
|
||||||
|
"virtualApiKeysDescription": "Vytvořit a spravovat manuální API klíče pro přístup k AI bráně veřejných AI brán",
|
||||||
|
"virtualApiKeysTabIdentity": "Klíče Identity",
|
||||||
|
"virtualApiKeysTabVirtual": "Virtuální Klíče",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Klíče Identity pro každého uživatele",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Každý uživatel již má klíč identity Pangolin pro tuto organizaci. Je jedinečný pro jejich účet a autentifikuje je k AI bránám, ke kterým mohou přistupovat.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Příklad",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Jak uživatelé získávají svůj klíč",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Navštivte URL veřejné AI brány v prohlížeči a přihlaste se pomocí svého účtu.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Nebo jděte na <url></url> pokud jste přihlášeni.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Můžete vytvořit další virtuální API klíče na kartě Virtuální Klíče. Tyto klíče mohou být zaměřeny na specifické veřejné AI brány a volitelně spojeny s uživatelem. Vytvoření klíče okamžitě poskytne přístup k vybraným veřejným AI bránám.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Manuálně vytvořte klíč",
|
||||||
|
"virtualApiKeysEmailIdentity": "Emailové klíče Identity",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Poslat každý vybraný uživatel nebo role jejich klíč identity Pangolin.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Odeslat všem uživatelům",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Odeslat každému členovi organizace, který má e-mail pro účet.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Uživatelé",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Role",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Odeslat e-maily",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Klíče identity zaslané e-mailem",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Bylo odesláno {sent} e-mailů.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} uživatelů bylo přeskočeno, protože nemají e-mailovou adresu.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Vyberte alespoň jednoho uživatele nebo roli, nebo odešlete všem uživatelům.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Chyba při odesílání klíčů identity",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Nepodařilo se odeslat e-mailem klíče identity",
|
||||||
|
"virtualApiKeysBannerTitle": "Klíče Identity pro každého uživatele",
|
||||||
|
"virtualApiKeysBannerDescription": "Každý uživatel již má klíč identity k dispozici na {keysUrl}. Můžete také ručně generovat klíče zde, které poskytují přímý přístup k veřejným AI bránám.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Zobrazit klíče Identity",
|
||||||
|
"virtualApiKeys": "Virtuální API Klíče",
|
||||||
|
"virtualApiKeysSearch": "Hledat klíče...",
|
||||||
|
"virtualApiKeysCreate": "Vytvořit Virtuální API Klíč",
|
||||||
|
"virtualApiKeysCreateDescription": "Ražte manuální klíč, který může volat veřejné AI brány v této organizaci",
|
||||||
|
"virtualApiKeysCreateButton": "Vytvořit Klíč",
|
||||||
|
"virtualApiKeysEmpty": "Žádné virtuální API klíče zatím nejsou",
|
||||||
|
"virtualApiKeysName": "Jméno",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Popis (nepovinné)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Přidružit uživatele (nepovinné)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Přidružte tento klíč k uživateli pro sledování použití. Jakmile je vytvořen, tento klíč okamžitě poskytne přístup k vybraným veřejným AI bránám. Nemusíte ručně přidružovat uživatele ke zdroji. Klíč se také zobrazí v profilu uživatele.",
|
||||||
|
"virtualApiKeysAllResources": "Všechny veřejné AI brány",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Povolte tento klíč pro přístup ke všem veřejným AI branám v organizaci",
|
||||||
|
"virtualApiKeysSelectResources": "Veřejné AI Brány",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Vybrat zdroje",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Zvolte, které veřejné AI brány tento klíč může přistupovat",
|
||||||
|
"virtualApiKeysNoResources": "Žádné zdroje",
|
||||||
|
"virtualApiKeysSecret": "Klíč",
|
||||||
|
"virtualApiKeysCopyKey": "Zkopírujte tento klíč. Později ho můžete znovu zobrazit z tabulky nebo při úpravě.",
|
||||||
|
"virtualApiKeysViewSecret": "Zobrazit tajemství",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Tajemství virtuálního API Klíče",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Toto tajemství poskytuje přístup ke veřejným AI branám přiřazeným k tomuto klíči",
|
||||||
|
"virtualApiKeysEdit": "Upravit Virtuální API Klíč",
|
||||||
|
"virtualApiKeysEditDescription": "Aktualizace spojeného uživatele a přístup k veřejným AI branám pro tento klíč",
|
||||||
|
"virtualApiKeysSaveButton": "Uložit změny",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Zvolte alespoň jednu veřejnou AI bránu, nebo povolte všechny veřejné AI brány",
|
||||||
|
"virtualApiKeysUpdated": "Aktualizován virtuální API klíč",
|
||||||
|
"virtualApiKeysUpdatedDescription": "Virtuální API klíč byl aktualizován",
|
||||||
|
"virtualApiKeysErrorUpdate": "Chyba při aktualizaci virtuálního API klíče",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Nepodařilo se aktualizovat virtuální API klíč",
|
||||||
|
"virtualApiKeysErrorCreate": "Chyba při vytváření virtuálního API klíče",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Nepodařilo se vytvořit virtuální API klíč",
|
||||||
|
"virtualApiKeysErrorDelete": "Chyba při mazání virtuálního API klíče",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Nepodařilo se smazat virtuální API klíč",
|
||||||
|
"virtualApiKeysDeleted": "Virtuální API klíč smazán",
|
||||||
|
"virtualApiKeysDeletedDescription": "Virtuální API klíč byl smazán",
|
||||||
|
"virtualApiKeysDelete": "Smazat virtuální API klíč",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Smazat klíč",
|
||||||
|
"virtualApiKeysQuestionRemove": "Jste si jisti, že chcete smazat tento virtuální API klíč?",
|
||||||
|
"virtualApiKeysMessageRemove": "Klienti používající tento klíč ztratí přístup okamžitě.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Chyba při načítání tajemství",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Nepodařilo se načíst tajemství virtuálního API klíče",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Nepřidělené",
|
||||||
|
"virtualApiKeysInferenceBudget": "Rozpočet",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Konfigurace, jak tento klíč omezí použití AI na základě výdajů nebo limitů tokenů",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Email klíče při generování",
|
||||||
|
"virtualApiKeysEmailThisKey": "Email tohoto klíče",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Odeslat klíč na spojeného uživatele a další adresy po jeho vytvoření",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Odeslat aktuální klíč spojenému uživateli a dalším adresám",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "Email není na tomto serveru nastaven",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Nastavit SMTP pro posílání virtuálních API klíčů.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Odeslat spojenému uživateli",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Email klíč na emailovou adresu spojeného uživatele",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Spojte uživatele pro poslání klíče uživateli",
|
||||||
|
"virtualApiKeysEmailAdditional": "Další e-maily",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Přidat e-mail a stiskněte Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Vyberte spojeného uživatele nebo přidejte alespoň jednu e-mailovou adresu",
|
||||||
|
"myVirtualApiKeysTitle": "Vaše API Klíče",
|
||||||
|
"myVirtualApiKeysDescription": "Zobrazte váš klíč identity a jakékoliv virtuální API klíče přiřazené k vám v této organizaci",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Vaše API klíče pro {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Zobrazte váš klíč identity a virtuální API klíče přiřazené k vám, které mohou přistupovat k {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Klíč identity",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Váš osobní API klíč",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Váš osobní klíč pro tuto organizaci. Je jedinečný pro váš účet a používá se k identifikaci, když využíváte AI brány.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Váš osobní API klíč pro {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Váš osobní klíč pro tuto organizaci. Použijte jej k volání {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Přiřazené klíče",
|
||||||
|
"myVirtualApiKeysManualDescription": "Manuální virtuální API klíče, které administrátor přiřadil k vašemu účtu",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Manuální virtuální API klíče spojené s vaším účtem, které mohou přistupovat k {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Zatím žádné přiřazené klíče",
|
||||||
|
"myVirtualApiKeysKindUser": "Identita",
|
||||||
|
"myVirtualApiKeysKindManual": "Manuální",
|
||||||
|
"myVirtualApiKeysUnnamed": "Nepojmenovaný klíč",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Odhalit tajemství",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Toto tajemství vás autentizuje k AI bránovým zdrojům",
|
||||||
|
"aiProvidersTitle": "AI Poskytovatelé",
|
||||||
|
"aiProvidersDescription": "Připojení poskytovatelů modelů pro AI úkoly v této organizaci",
|
||||||
|
"aiProvidersBannerTitle": "Připojit poskytovatele modelů",
|
||||||
|
"aiProvidersBannerDescription": "Poskytovatelé jsou modelové backendy, které Pangolin používá pro AI úkoly. Připojte sem OpenAI, Anthropic a další dodavatele, poté je připojte k veřejným AI branám, aby uživatelé mohli volat modely s přístupem podmíněným na základě identity, rozpočtů a protokolování.",
|
||||||
|
"aiProvidersAdd": "Přidat poskytovatele",
|
||||||
|
"aiProvidersSearch": "Hledat poskytovatele...",
|
||||||
|
"aiProvidersEmpty": "Zatím žádní poskytovatelé AI nejsou",
|
||||||
|
"aiProviderCreate": "Vytvořit poskytovatele AI",
|
||||||
|
"aiProviderCreateDescription": "Přidat modelového poskytovatele pro tuto organizaci",
|
||||||
|
"aiProviderSeeAll": "Zobrazit všechny poskytovatele",
|
||||||
|
"aiProviderSetting": "Nastavení poskytovatele pro {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Konfigurace tohoto AI poskytovatele",
|
||||||
|
"aiProviderGeneral": "Obecný",
|
||||||
|
"aiProviderGeneralDescription": "Základní nastavení pro tohoto poskytovatele",
|
||||||
|
"aiProviderConfiguration": "Konfigurace",
|
||||||
|
"aiProviderConfigurationDescription": "Síťové směrování a autentizace pro tohoto poskytovatele",
|
||||||
|
"aiProviderNetworkSettings": "Síťové nastavení",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Vyberte, jak dosáhne provoz tohoto poskytovatele",
|
||||||
|
"aiProviderAuthSettings": "Autentifikace",
|
||||||
|
"aiProviderAuthSettingsDescription": "Nastavte, jak tento poskytovatel ověřuje požadavky na svou upstream URL",
|
||||||
|
"aiProviderBudgetSettings": "Rozpočet",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Konfigurace, jak tento poskytovatel omezuje použití na základě výdajů nebo limitů tokenů",
|
||||||
|
"aiBudgetAdd": "Přidat rozpočet",
|
||||||
|
"aiBudgetEmpty": "Zatím nebyly nakonfigurovány žádné rozpočty. Kliknutím na Přidat rozpočet nastavíte limit výdajů nebo tokenů.",
|
||||||
|
"aiBudgetUnit": "Typ výdajů",
|
||||||
|
"aiBudgetPeriod": "Období resetování",
|
||||||
|
"aiBudgetAmount": "Maximální výdaje",
|
||||||
|
"aiBudgetAmountPlaceholder": "Maximální výdaje",
|
||||||
|
"aiBudgetPeriodHourly": "Hodinově",
|
||||||
|
"aiBudgetPeriodDaily": "Denně",
|
||||||
|
"aiBudgetPeriodWeekly": "Týdně",
|
||||||
|
"aiBudgetPeriodMonthly": "Měsíčně",
|
||||||
|
"aiBudgetPeriodYearly": "Ročně",
|
||||||
|
"aiBudgetPeriodLifetime": "Po celou dobu",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Tokeny",
|
||||||
|
"aiBudgetConflictError": "Rozpočet pro toto období resetu a typ výdajů již existuje",
|
||||||
|
"aiBudgetInvalidAmountError": "Zadejte maximální výdaje větší než 0",
|
||||||
|
"aiBudgetUpdated": "Rozpočty aktualizovány",
|
||||||
|
"aiBudgetErrorSave": "Nepodařilo se aktualizovat rozpočty",
|
||||||
|
"aiProviderType": "Typ poskytovatele",
|
||||||
|
"aiProviderTypeSearch": "Hledat poskytovatele...",
|
||||||
|
"aiProviderTypeNotFound": "Typ poskytovatele nenalezen",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "Vlastní",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "Přineste si svůj vlastní koncový bod nebo směrujte prostřednictvím cílů stránek",
|
||||||
|
"aiProviderUpstreamUrl": "Upstream URL",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Základní URL pro API poskytovatele",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Nechte prázdné pro použití výchozí upstream URL pro tohoto poskytovatele",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Efektivní upstream URL",
|
||||||
|
"aiProviderApiKey": "API Klíč",
|
||||||
|
"aiProviderApiKeyDescription": "API klíč používaný k ověření požadavků tohoto poskytovatele",
|
||||||
|
"aiProviderCustomHeadersDescription": "Záhlaví poslaná v každém požadavku k tomuto poskytovateli. Nové řádky: Název-Záhlaví: hodnota",
|
||||||
|
"aiProviderApiKeyLastChars": "API Klíč",
|
||||||
|
"aiProviderAuthType": "Typ ověření",
|
||||||
|
"aiProviderAuthTypeSearch": "Vyhledat typy ověření...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Nebyl nalezen žádný typ ověření",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Oprávnění: Bearer klíč. Používá OpenAI a většina poskytovatelů",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key záhlaví. Používá Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key záhlaví. Používá Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Oprávnění: Splunk klíč. Používá Splunk HTTP Event Collector",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer klíč. Používá Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "Bez ověření",
|
||||||
|
"aiProviderAuthTypePassthrough": "Průchod",
|
||||||
|
"aiProviderAuthTypeDescription": "Jak upstream API ověřuje požadavky",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Přepošlete API klíče volajícího na upstream",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Neodesílejte autentizační záhlaví na upstream",
|
||||||
|
"aiProviderRoutingMode": "Režim směrování",
|
||||||
|
"aiProviderRoutingModeDescription": "Odesílejte provoz na upstream URL nebo na HTTP cíle na vašich stránkách",
|
||||||
|
"aiProviderRoutingModeUrl": "Upstream URL",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Zavolejte veřejný nebo soukromý základní URL API",
|
||||||
|
"aiProviderRoutingModeTarget": "Cíle stránek",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Směrujte přes cíle na svých stránkách",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Po vytvoření tohoto poskytovatele, nakonfigurujte cíle stránek na záložce Nastavení sítě.",
|
||||||
|
"aiProviderTargetNoOne": "Tento poskytovatel nemá žádné cíle. Přidejte cíl pro směrování požadavků přes vaše stránky.",
|
||||||
|
"aiProviderSkipTlsVerification": "Přeskočit ověření TLS",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Zakázat ověření certifikátu TLS pro upstream připojení",
|
||||||
|
"aiProviderBudget": "Rozpočet",
|
||||||
|
"aiProviderBudgetDescription": "Volitelný rozpočet na výdaje nebo tokeny pro tohoto poskytovatele",
|
||||||
|
"aiProviderBudgetAmount": "Částka rozpočtu",
|
||||||
|
"aiProviderBudgetUnit": "Jednotka rozpočtu",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Tokeny",
|
||||||
|
"aiProviderEnabled": "Povoleno",
|
||||||
|
"aiProviderEnabledDescription": "Plně deaktivujte tohoto poskytovatele u všech zdrojů",
|
||||||
|
"aiProviderErrorCreate": "Nepodařilo se vytvořit poskytovatele AI",
|
||||||
|
"aiProviderErrorUpdate": "Nepodařilo se aktualizovat poskytovatele AI",
|
||||||
|
"aiProviderErrorDelete": "Nepodařilo se smazat poskytovatele AI",
|
||||||
|
"aiProviderErrorLoad": "Nepodařilo se načíst poskytovatele AI",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Zadejte platnou upstream URL",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Upstream URL je vyžadováno pro tohoto poskytovatele",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Typ ověření je povinný",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API klíč je povinný",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Směrování cílů stránek je k dispozici pouze pro vlastní poskytovatele",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Vyberte alespoň jednu schopnost API",
|
||||||
|
"aiProviderCapabilities": "Schopnosti API",
|
||||||
|
"aiProviderCapabilitiesDescription": "Vyberte, které formáty API tento poskytovatel zvládne. Známí poskytovatelé začínají s doporučenými výchozími hodnotami.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Vyberte, které formáty API tento vlastní poskytovatel zvládne",
|
||||||
|
"aiProviderCapabilitiesSelect": "Vyberte možnosti",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Žádné možnosti nenalezeny",
|
||||||
|
"aiProviderCapabilitiesSearch": "Hledat možnosti...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Chat Completion",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Podporuje /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "Odpovědi OpenAI",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Podporuje /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Zprávy Anthropic",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Podporuje /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Generování obsahu Gemini",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Podporuje přímé API Gemini",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Vyvolání modelu Bedrock",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Podporuje Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex generate content",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Podporuje Vertex AI Gemini formát",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Předpověď z Vertex Raw",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Podporuje Vertex AI rawPredict pro Anthropic modely",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock Konverzace",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Podporuje Amazon Bedrock Converse API",
|
||||||
|
"aiProviderCreated": "AI poskytovatel vytvořen",
|
||||||
|
"aiProviderUpdated": "AI poskytovatel aktualizován",
|
||||||
|
"aiProviderDeleted": "AI poskytovatel odstraněn",
|
||||||
|
"aiProviderDelete": "Odstranit poskytovatele",
|
||||||
|
"aiProviderDeleteConfirm": "Odstranit poskytovatele",
|
||||||
|
"aiProviderQuestionRemove": "Opravdu chcete odstranit tohoto AI poskytovatele?",
|
||||||
|
"aiProviderMessageRemove": "Tímto bude trvale odstraněn poskytovatel a jeho modely a cíle. To nelze vrátit zpět.",
|
||||||
|
"aiProviderErrorNoUpdate": "AI poskytovatele nelze aktualizovat",
|
||||||
|
"aiProviderModels": "Modely",
|
||||||
|
"aiProviderModelsDescription": "Definujte seznamy pro povolení a blokování pro tohoto poskytovatele. Požadavky musí odpovídat seznamu povolení a nesmí odpovídat seznamu blokací.",
|
||||||
|
"aiProviderCreateModelsDescription": "Vyberte, které modely tento poskytovatel může nabízet. Prázdný seznam povolení zamítá veškerý provoz. Modely můžete přidat nebo změnit později.",
|
||||||
|
"aiProviderModelsPlaceholder": "Hledat modely nebo zadat vlastní klíč",
|
||||||
|
"aiProviderModelsAllow": "Seznam povolení",
|
||||||
|
"aiProviderModelsAllowDescription": "Modely, které lze použít prostřednictvím tohoto poskytovatele. Prázdný znamená zamítnout vše.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Zadejte klíč modelu",
|
||||||
|
"aiProviderModelsAllowEmpty": "Ještě nebyly přidány žádné modely.",
|
||||||
|
"aiProviderModelsBlock": "Seznam blokací",
|
||||||
|
"aiProviderModelsBlockDescription": "Modely zamítnuté i když odpovídají záznamům ze seznamu povolení.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Zadejte klíč modelu",
|
||||||
|
"aiProviderModelsBlockEmpty": "Ještě nebyly přidány žádné modely.",
|
||||||
|
"aiProviderModelsAdd": "Přidat modely",
|
||||||
|
"aiProviderModelsClearAll": "Vymazat vše",
|
||||||
|
"aiProviderModelsAddCustom": "Přidat \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Stiskněte Enter pro přidání tohoto vlastního klíče modelu.",
|
||||||
|
"aiProviderModelsAddBulk": "Přidat {count} vlastních klíčů",
|
||||||
|
"aiProviderModelsAddBulkHint": "Stiskněte Enter pro přidání {count} vlastních klíčů.",
|
||||||
|
"aiProviderModelsAddOne": "Přidat {key} nyní",
|
||||||
|
"aiProviderModelsAddSelected": "Přidat vybrané",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} vybráno",
|
||||||
|
"aiProviderModelsSelectAll": "Vybrat vše",
|
||||||
|
"aiProviderModelsClearSelected": "Smazat",
|
||||||
|
"aiProviderModelsBulkHint": "Vyberte známé modely nebo zadejte vlastní klíč.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Žádné odpovídající modely v katalogu.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Známé modely",
|
||||||
|
"aiProviderModelsAllLabel": "Všechny modely",
|
||||||
|
"aiProviderModelsAllPatternHint": "Zástupný znak: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Povolit všechny modely",
|
||||||
|
"aiProviderModelsAddAllBlock": "Blokovat všechny modely",
|
||||||
|
"aiProviderModelsAddAllDescription": "Používá * wildcard tak, aby odpovídal na všechny klíče modelů.",
|
||||||
|
"aiProviderModelsViewMore": "Zobrazit více ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Zobrazit méně",
|
||||||
|
"aiProviderModelsRemove": "Odstranit model",
|
||||||
|
"aiProviderModelsEditHint": "Klikněte pro úpravu nastavení modelu",
|
||||||
|
"aiProviderModelsSourceCatalog": "Známý model katalogu",
|
||||||
|
"aiProviderModelsSourceCustom": "Vlastní klíč modelu",
|
||||||
|
"aiProviderModelsSourcePattern": "Vzorek wildcard",
|
||||||
|
"aiProviderModelsSourceAll": "Odpovídá každému klíči modelu",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Rozpočet nakonfigurován",
|
||||||
|
"aiProviderModelsEditTitle": "Upravit model",
|
||||||
|
"aiProviderModelsEditDescription": "Aktualizujte klíč modelu nebo nakonfigurujte jeho rozpočet.",
|
||||||
|
"aiProviderModelsBudgetTab": "Rozpočet",
|
||||||
|
"aiProviderModelsKeyLabel": "Klíč modelu",
|
||||||
|
"aiProviderModelsKeyRequired": "Zadejte klíč modelu",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Tento klíč modelu je již na seznamu",
|
||||||
|
"aiProviderModelsOverlapError": "Tyto vzory nemohou být na obou seznamech: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modely aktualizovány",
|
||||||
|
"aiProviderModelsErrorUpdate": "Nepodařilo se aktualizovat modely",
|
||||||
|
"aiResourceProviders": "Poskytovatelé",
|
||||||
|
"aiResourceProvidersDescription": "Zvolte, jaké AI poskytovatelé mohou tento AI gateway použít",
|
||||||
|
"aiResourceProvidersHelp": "Přidejte poskytovatele a zvolte dědění (použijte seznamy každého z nich) nebo vyberte (zvolte seznam povolení pro tento zdroj). Povolené vzory, které se střetávají mezi přidanými poskytovateli nejsou povoleny.",
|
||||||
|
"aiResourceProvidersSelect": "Vyberte poskytovatele",
|
||||||
|
"aiResourceProvidersEmpty": "AI poskytovatelé nenalezeni",
|
||||||
|
"aiResourceProvidersNoneAttached": "Žádný poskytovatel nebyl zatím připojen.",
|
||||||
|
"aiResourceProvidersAdd": "Přidat poskytovatele",
|
||||||
|
"aiResourceProvidersRemove": "Odstranit poskytovatele",
|
||||||
|
"aiResourceProviderToggleEnabled": "Povolit nebo zakázat tohoto poskytovatele na zdroji",
|
||||||
|
"aiResourceProviderDisabled": "Zakázáno",
|
||||||
|
"aiResourceProvidersUpdated": "Poskytovatelé aktualizováni",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Nepodařilo se aktualizovat poskytovatele",
|
||||||
|
"aiResourceProviderEditDescription": "Zvolte, jak bude tento poskytovatelův model vystaven na tomto zdroji.",
|
||||||
|
"viewProviderSettings": "Zobrazit nastavení poskytovatele",
|
||||||
|
"aiResourceProviderMode": "Režim přístupu",
|
||||||
|
"aiResourceProviderModeInherit": "Dědit",
|
||||||
|
"aiResourceProviderModeSelect": "Vybrat",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Vyberte · {count} modely",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Použít seznamy povolení a blokací poskytovatele, jak byly konfigurovány.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Vyberte podmnožinu modelů povolených pro tento zdroj.",
|
||||||
|
"aiResourceProviderAllowModels": "Seznam povolení",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Vybrat modely",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Hledat modely...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Žádné modely nenalezeny",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Lze vybrat pouze modely ze seznamu povolení poskytovatele.",
|
||||||
|
"aiResourceAliasRequired": "Alias je povinný pro AI brány",
|
||||||
|
"aiResourceDomainConfiguration": "Konfigurace domény",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Vyberte doménu, kterou klienti použijí k přístupu ke této AI bráně.",
|
||||||
|
"aiUsageAnalyticsTitle": "AI analytika využití",
|
||||||
|
"aiUsageAnalyticsDescription": "Analyzujte náklady na AI brány, použití tokenů a aktivitu napříč poskytovateli, zdroji, rolemi a uživateli",
|
||||||
|
"aiUsageTabOverview": "Přehled",
|
||||||
|
"aiUsageTabProviders": "Poskytovatelé",
|
||||||
|
"aiUsageTabResources": "Zdroje",
|
||||||
|
"aiUsageFilterProvider": "Poskytovatel",
|
||||||
|
"aiUsageFilterModel": "Model",
|
||||||
|
"aiUsageFilterResource": "Zdroj",
|
||||||
|
"aiUsageFilterRole": "Role",
|
||||||
|
"aiUsageFilterUser": "Uživatel",
|
||||||
|
"aiUsageFilterAllProviders": "Všichni poskytovatelé",
|
||||||
|
"aiUsageFilterAllModels": "Všechny modely",
|
||||||
|
"aiUsageFilterAllResources": "Všechny zdroje",
|
||||||
|
"aiUsageFilterAllRoles": "Všechny role",
|
||||||
|
"aiUsageFilterAllUsers": "Všichni uživatelé",
|
||||||
|
"aiUsageFilterSearch": "Hledat...",
|
||||||
|
"aiUsageFilterNotFound": "Žádné možnosti nenalezeny",
|
||||||
|
"aiUsageResetFilters": "Resetovat filtry",
|
||||||
|
"aiUsageRefresh": "Obnovit",
|
||||||
|
"aiUsageTokenTypePrompt": "Výzvy",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Čtení z vyrovnávací paměti",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Zápis do vyrovnávací paměti",
|
||||||
|
"aiUsageTokenTypeCompletion": "Doplňování",
|
||||||
|
"aiUsageTokenTypeReasoning": "Odůvodnění",
|
||||||
|
"aiUsageRequests": "Požadavky",
|
||||||
|
"aiUsageCost": "Náklady",
|
||||||
|
"aiUsageTokens": "Tokeny",
|
||||||
|
"aiUsageOther": "Ostatní",
|
||||||
|
"aiUsageTotalRequests": "Celkové požadavky",
|
||||||
|
"aiUsageTotalTokens": "Celkem tokenů",
|
||||||
|
"aiUsageTotalCost": "Celkové náklady",
|
||||||
|
"aiUsageEstimated": "Odhadováno",
|
||||||
|
"aiUsageRequestVolume": "Objem požadavků",
|
||||||
|
"aiUsageTokenUsage": "Použití tokenu",
|
||||||
|
"aiUsageModelCost": "Náklady na model",
|
||||||
|
"aiUsageModelTokens": "Modelové tokeny",
|
||||||
|
"aiUsageTopModels": "Nejoblíbenější modely",
|
||||||
|
"aiUsageTopProviders": "Nejoblíbenější poskytovatelé",
|
||||||
|
"aiUsageProviderCost": "Náklady poskytovatele",
|
||||||
|
"aiUsageProviderTokenUsage": "Použití tokenů poskytovatele",
|
||||||
|
"aiUsageTopResources": "Nejoblíbenější zdroje",
|
||||||
|
"aiUsageResourceCost": "Náklady na zdroje",
|
||||||
|
"aiUsageResourceTokenUsage": "Použití tokenů zdroje",
|
||||||
|
"aiUsageResourceTypePublic": "Veřejný zdroj",
|
||||||
|
"aiUsageResourceTypeSite": "Soukromý zdroj",
|
||||||
|
"aiUsageNoResource": "Žádný zdroj",
|
||||||
|
"aiUsageRolesTab": "Role",
|
||||||
|
"aiUsageUsersTab": "Uživatelé",
|
||||||
|
"aiUsageTopRoles": "Nejoblíbenější role",
|
||||||
|
"aiUsageRoleCost": "Náklady role",
|
||||||
|
"aiUsageRoleTokenUsage": "Použití tokenů role",
|
||||||
|
"aiUsageTopUsers": "Nejoblíbenější uživatelé",
|
||||||
|
"aiUsageUserCost": "Náklady uživatele",
|
||||||
|
"aiUsageUserTokenUsage": "Použití tokenů uživatelem",
|
||||||
|
"aiUsageUnknownUser": "Neznámý uživatel",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Virtuální API klíče",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Virtuální API klíč",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Všechny virtuální API klíče",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Nejoblíbenější virtuální API klíče",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Náklady na virtuální API klíč",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Použití tokenů virtuálního API klíče",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Žádný virtuální API klíč",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Nepojmenovaný klíč",
|
||||||
|
"aiUsageLoading": "Nahrávání...",
|
||||||
|
"aiUsageNoData": "Žádná data",
|
||||||
|
"resourceBudgetSettings": "Rozpočet",
|
||||||
|
"resourceBudgetSettingsDescription": "Zjistěte, jak tato AI brána omezuje užívání na základě výdajů nebo limitů tokenů",
|
||||||
"sidebarApiKeys": "API klíče",
|
"sidebarApiKeys": "API klíče",
|
||||||
"sidebarProvisioning": "Zajištění",
|
"sidebarProvisioning": "Zajištění",
|
||||||
"sidebarSettings": "Nastavení",
|
"sidebarSettings": "Nastavení",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Podsíť",
|
"subnetPlaceholder": "Podsíť",
|
||||||
"addressDescription": "Interní adresa klienta. Musí spadat do podsítě organizace.",
|
"addressDescription": "Interní adresa klienta. Musí spadat do podsítě organizace.",
|
||||||
"selectSites": "Vyberte stránky",
|
"selectSites": "Vyberte stránky",
|
||||||
|
"selectResources": "Vyberte zdroje",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# zdroj} few {# zdroje} many {# zdrojů} other {# zdroje}}",
|
||||||
"selectLabels": "Vyberte názvy",
|
"selectLabels": "Vyberte názvy",
|
||||||
"sitesDescription": "Klient bude mít připojení k vybraným webům",
|
"sitesDescription": "Klient bude mít připojení k vybraným webům",
|
||||||
"clientInstallOlm": "Nainstalovat Olm",
|
"clientInstallOlm": "Nainstalovat Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Čas je v sekundách",
|
"timeIsInSeconds": "Čas je v sekundách",
|
||||||
"requireDeviceApproval": "Vyžadovat schválení zařízení",
|
"requireDeviceApproval": "Vyžadovat schválení zařízení",
|
||||||
"requireDeviceApprovalDescription": "Uživatelé s touto rolí potřebují nová zařízení schválená správcem, než se mohou připojit a přistupovat ke zdrojům.",
|
"requireDeviceApprovalDescription": "Uživatelé s touto rolí potřebují nová zařízení schválená správcem, než se mohou připojit a přistupovat ke zdrojům.",
|
||||||
"sshSettings": "Nastavení SSH",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI brána",
|
||||||
"sshAccess": "SSH Přístup",
|
"sshAccess": "SSH Přístup",
|
||||||
"rdpSettings": "Nastavení RDP",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "Nastavení VNC",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH server",
|
"sshServer": "SSH server",
|
||||||
"rdpServer": "RDP server",
|
"rdpServer": "RDP server",
|
||||||
"vncServer": "VNC server",
|
"vncServer": "VNC server",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI brána",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Schéma",
|
"editInternalResourceDialogScheme": "Schéma",
|
||||||
"editInternalResourceDialogEnableSsl": "Povolit TLS",
|
"editInternalResourceDialogEnableSsl": "Povolit TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Povolit ICMP ping",
|
"privateResourceAllowIcmpPing": "Povolit ICMP ping",
|
||||||
"privateResourceNetworkAccess": "Přístup k síti",
|
"privateResourceNetworkAccess": "Přístup k síti",
|
||||||
"privateResourceNetworkAccessDescription": "Ovládejte přístup k TCP/UDP portům a zda je pro tento zdroj povolen ICMP ping.",
|
"privateResourceNetworkAccessDescription": "Ovládejte přístup k TCP/UDP portům a zda je pro tento zdroj povolen ICMP ping.",
|
||||||
"hostSettings": "Nastavení hostitele",
|
"hostSettings": "Hostitel",
|
||||||
"cidrSettings": "Nastavení CIDR",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Vlastnosti zdroje",
|
"createInternalResourceDialogResourceProperties": "Vlastnosti zdroje",
|
||||||
"createInternalResourceDialogName": "Jméno",
|
"createInternalResourceDialogName": "Jméno",
|
||||||
"createInternalResourceDialogSite": "Lokalita",
|
"createInternalResourceDialogSite": "Lokalita",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI brána",
|
||||||
"scheme": "Schéma",
|
"scheme": "Schéma",
|
||||||
"createInternalResourceDialogScheme": "Schéma",
|
"createInternalResourceDialogScheme": "Schéma",
|
||||||
"createInternalResourceDialogEnableSsl": "Povolit TLS",
|
"createInternalResourceDialogEnableSsl": "Povolit TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Platné heslo",
|
"validPassword": "Platné heslo",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Platný virtuální API klíč",
|
||||||
"view": "Zobrazit",
|
"view": "Zobrazit",
|
||||||
"configManaged": "Správa konfigurace",
|
"configManaged": "Správa konfigurace",
|
||||||
"connectedClient": "Připojený klient",
|
"connectedClient": "Připojený klient",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Zaznamenávání záznamu",
|
"logRetention": "Zaznamenávání záznamu",
|
||||||
"logRetentionDescription": "Spravovat, jak dlouho jsou různé typy logů uloženy pro tuto organizaci nebo je zakázat",
|
"logRetentionDescription": "Spravovat, jak dlouho jsou různé typy logů uloženy pro tuto organizaci nebo je zakázat",
|
||||||
"requestLogsDescription": "Zobrazit podrobné protokoly požadavků pro zdroje v této organizaci",
|
"requestLogsDescription": "Zobrazit podrobné protokoly požadavků pro zdroje v této organizaci",
|
||||||
|
"aiSessionLogs": "Protokoly AI Gateway Session",
|
||||||
|
"aiSessionLogsDescription": "Zobrazit uložené výzvy a odpovědi na žádosti AI brány v této organizaci",
|
||||||
|
"sidebarLogsAi": "Protokoly relací",
|
||||||
|
"commandLogsAi": "Protokoly relací",
|
||||||
|
"sidebarLogsAiUsage": "Analýza využití",
|
||||||
|
"commandLogsAiUsage": "Analýza využití",
|
||||||
|
"provider": "Poskytovatel",
|
||||||
|
"capability": "Funkce",
|
||||||
|
"model": "Model",
|
||||||
|
"virtualApiKey": "Virtuální API klíč",
|
||||||
|
"noVirtualApiKey": "Žádný virtuální API klíč",
|
||||||
|
"stream": "Proud",
|
||||||
|
"streaming": "Streamování",
|
||||||
|
"nonStreaming": "Nestreamování",
|
||||||
|
"statusCode": "Kód stavu",
|
||||||
|
"aiSessionId": "ID relace",
|
||||||
|
"aiSessionRequest": "Požadavek",
|
||||||
|
"aiSessionResponse": "Odpověď",
|
||||||
|
"aiSessionNoData": "Žádná data nezachycena",
|
||||||
|
"aiSessionCouldNotParse": "(surové, nelze parse protokol)",
|
||||||
|
"aiSessionLogTruncated": "Tato relace byla oříznuta před uložením a může být neúplná.",
|
||||||
|
"aiSessionViewRaw": "Zobrazit syrové JSON",
|
||||||
|
"aiSessionViewChat": "Zobrazit chat",
|
||||||
|
"cost": "Náklady",
|
||||||
|
"estimated": "Odhadovaný",
|
||||||
|
"tokenUsage": "Použití tokenu",
|
||||||
|
"promptTokens": "Výzvy tokenů",
|
||||||
|
"cacheReadTokens": "Číst tokeny z vyrovnávací paměti",
|
||||||
|
"cacheWriteTokens": "Zapisovat tokeny do vyrovnávací paměti",
|
||||||
|
"completionTokens": "Doplňovací tokeny",
|
||||||
|
"reasoningTokens": "Odůvodňující tokeny",
|
||||||
|
"totalTokens": "Celkem tokenů",
|
||||||
"requestAnalyticsDescription": "Zobrazit podrobnou analýzu požadavků pro zdroje v této organizaci",
|
"requestAnalyticsDescription": "Zobrazit podrobnou analýzu požadavků pro zdroje v této organizaci",
|
||||||
"logRetentionRequestLabel": "Zachování logu HTTP požadavků",
|
"logRetentionRequestLabel": "Zachování logu HTTP požadavků",
|
||||||
"logRetentionRequestDescription": "Jak dlouho uchovávat záznamy požadavků",
|
"logRetentionRequestDescription": "Jak dlouho uchovávat záznamy požadavků",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Jak dlouho uchovávat záznamy akcí",
|
"logRetentionActionDescription": "Jak dlouho uchovávat záznamy akcí",
|
||||||
"logRetentionConnectionLabel": "Uchovávání protokolu připojení",
|
"logRetentionConnectionLabel": "Uchovávání protokolu připojení",
|
||||||
"logRetentionConnectionDescription": "Jak dlouho uchovávat protokoly připojení",
|
"logRetentionConnectionDescription": "Jak dlouho uchovávat protokoly připojení",
|
||||||
|
"logRetentionAISessionsLabel": "Retence protokolu AI Gateway Session",
|
||||||
|
"logRetentionAISessionsDescription": "Jak dlouho zachovávat protokoly relací výzev/odpovědí AI brány",
|
||||||
"logRetentionDisabled": "Zakázáno",
|
"logRetentionDisabled": "Zakázáno",
|
||||||
"logRetention3Days": "3 dny",
|
"logRetention3Days": "3 dny",
|
||||||
"logRetention7Days": "7 dní",
|
"logRetention7Days": "7 dní",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Zdrojová adresa",
|
"sourceAddress": "Zdrojová adresa",
|
||||||
"destinationAddress": "Cílová adresa",
|
"destinationAddress": "Cílová adresa",
|
||||||
"duration": "Doba trvání",
|
"duration": "Doba trvání",
|
||||||
"licenseRequiredToUse": "Pro použití této funkce je vyžadována licence <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> nebo <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Zarezervujte si bezplatné demo nebo POC zkušební verzi, abyste se dozvěděli více.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Licence Enterprise Edition</enterpriseEditionLink> je nutná. <bookADemoLink>Zarezervujte si ukázku nebo vyzkoušení</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> je vyžadována pro použití této funkce. Tato funkce je také k dispozici v <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Rezervujte si demo nebo POC zkušební verzi zdarma, abyste se dozvěděli více.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Licence Enterprise Edition</enterpriseEditionLink> je nutná. <bookADemoLink>Zarezervujte si ukázku nebo vyzkoušení</bookADemoLink>",
|
||||||
"certResolver": "Oddělovač certifikátů",
|
"certResolver": "Oddělovač certifikátů",
|
||||||
"certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.",
|
"certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.",
|
||||||
"selectCertResolver": "Vyberte řešič certifikátů",
|
"selectCertResolver": "Vyberte řešič certifikátů",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Výběrem více webů se povolí odolné směrování a přepojení pro vysokou dostupnost.",
|
"internalResourceFormMultiSiteRoutingHelp": "Výběrem více webů se povolí odolné směrování a přepojení pro vysokou dostupnost.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Zjistit více",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Zjistit více",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Omezte přístup na specifické TCP/UDP porty nebo povolte/blokujte všechny porty.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Omezte přístup na specifické TCP/UDP porty nebo povolte/blokujte všechny porty.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "Konfigurace HTTP",
|
"createInternalResourceDialogHttpConfiguration": "Konfigurace domény",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Zvolte doménu, kterou klienti použijí k dosažení tohoto zdroje přes HTTP nebo HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Zvolte doménu, kterou klienti použijí k dosažení tohoto zdroje přes HTTP nebo HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "Konfigurace HTTP",
|
"editInternalResourceDialogHttpConfiguration": "Konfigurace domény",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Zvolte doménu, kterou klienti použijí k dosažení tohoto zdroje přes HTTP nebo HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Zvolte doménu, kterou klienti použijí k dosažení tohoto zdroje přes HTTP nebo HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Bez nálepky",
|
"resourceLauncherUnlabeled": "Bez nálepky",
|
||||||
|
"resourceLauncherAiGateway": "AI brána",
|
||||||
"resourceLauncherNoSite": "Žádná lokalita",
|
"resourceLauncherNoSite": "Žádná lokalita",
|
||||||
|
"resourceLauncherAvailableModels": "Dostupné modely",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modely, které můžete použít s touto AI bránou.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Pro tuto službu nejsou k dispozici žádné modely.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Nelze načíst dostupné modely.",
|
||||||
|
"resourceLauncherApiKeys": "API klíče",
|
||||||
|
"resourceLauncherApiKeysDescription": "Použijte svůj klíč identity nebo přidělený klíč k autentizaci s tímto zdrojem.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Klíč identity",
|
||||||
|
"resourceLauncherApiKeysManual": "Přidělené klíče",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Pro tuto službu nejsou k dispozici žádné klíče API.",
|
||||||
|
"resourceLauncherApiKeysError": "Nelze načíst API klíče.",
|
||||||
"resourceLauncherNoResourcesInGroup": "V této skupině nejsou žádné zdroje",
|
"resourceLauncherNoResourcesInGroup": "V této skupině nejsou žádné zdroje",
|
||||||
"resourceLauncherEmptyStateTitle": "Žádné dostupné zdroje",
|
"resourceLauncherEmptyStateTitle": "Žádné dostupné zdroje",
|
||||||
"resourceLauncherEmptyStateDescription": "Zatím nemáte přístup k žádným zdrojům. Kontaktujte svého administrátora, abyste požádali o přístup.",
|
"resourceLauncherEmptyStateDescription": "Zatím nemáte přístup k žádným zdrojům. Kontaktujte svého administrátora, abyste požádali o přístup.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Nepodařilo se smazat spouštěcí pohled. Prosím zkuste to znovu.",
|
"resourceLauncherViewDeleteFailedDescription": "Nepodařilo se smazat spouštěcí pohled. Prosím zkuste to znovu.",
|
||||||
"memberPortalPrevious": "Předchozí",
|
"memberPortalPrevious": "Předchozí",
|
||||||
"memberPortalNext": "Následující",
|
"memberPortalNext": "Následující",
|
||||||
"httpSettings": "Nastavení HTTP",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "Nastavení TCP",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "Nastavení UDP",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Zřizování bezpečného připojení…",
|
"sshConnectingDescription": "Zřizování bezpečného připojení…",
|
||||||
"sshConnecting": "Připojení…",
|
"sshConnecting": "Připojení…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Venligst tjek at du er loget ind som korrekt bruger.",
|
"inviteLoginUser": "Venligst tjek at du er loget ind som korrekt bruger.",
|
||||||
"inviteErrorNoUser": "Beklager, men det ser ud til, at invitationen du prøver at få adgang til, ikke er til en eksisterende bruger.",
|
"inviteErrorNoUser": "Beklager, men det ser ud til, at invitationen du prøver at få adgang til, ikke er til en eksisterende bruger.",
|
||||||
"inviteCreateUser": "Opret venligst en konto først.",
|
"inviteCreateUser": "Opret venligst en konto først.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Invitationer kan kun accepteres af interne konti. Log ud og log ind med din adgangskode til denne e-mail.",
|
||||||
|
"inviteLoginInternalOnly": "Invitationer kræver en intern konto med en adgangskode. Opret en konto eller log ind med din adgangskode.",
|
||||||
"goHome": "Gå hjem",
|
"goHome": "Gå hjem",
|
||||||
"inviteLogInOtherUser": "Log ind som en anden bruger",
|
"inviteLogInOtherUser": "Log ind som en anden bruger",
|
||||||
"createAnAccount": "Opret konto",
|
"createAnAccount": "Opret konto",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Ingen land fundet.",
|
"noCountryFound": "Ingen land fundet.",
|
||||||
"siteSelectionDescription": "Dette site vil give forbindelse til målet.",
|
"siteSelectionDescription": "Dette site vil give forbindelse til målet.",
|
||||||
"resourceType": "Ressourcetype",
|
"resourceType": "Ressourcetype",
|
||||||
"resourceTypeDescription": "Dette styrer ressourceprotokolen, og hvordan den vises i browseren. Dette kan ikke ændres senere.",
|
"resourceTypeDescription": "Dette styrer ressourceprotokollen og kan ikke ændres senere.",
|
||||||
|
"resourceTypeSearch": "Søg ressource typer...",
|
||||||
|
"resourceTypeNotFound": "Ingen ressource type fundet",
|
||||||
|
"resourceTypeHttpDescription": "Proxy webtrafik over HTTPS ved brug af et domænenavn",
|
||||||
|
"resourceTypeInferenceDescription": "Rute AI-anmodninger gennem vedhæftede udbydere",
|
||||||
|
"resourceTypeSshDescription": "Adgang til en SSH-server fra browseren",
|
||||||
|
"resourceTypeRdpDescription": "Adgang til et fjernskrivebord fra browseren",
|
||||||
|
"resourceTypeVncDescription": "Adgang til et VNC-skrivebord fra browseren",
|
||||||
|
"resourceTypeTcpDescription": "Proxy rå TCP-trafik ved at bruge et portnummer",
|
||||||
|
"resourceTypeUdpDescription": "Proxy rå UDP-trafik ved at bruge et portnummer",
|
||||||
|
"privateResourceTypeDescription": "Dette styrer, hvordan klienter når ressourcen. Dette kan ikke ændres senere.",
|
||||||
|
"privateResourceTypeHostDescription": "Eksponer en enkelt vært på sitenettet mod tilkoblede klienter",
|
||||||
|
"privateResourceTypeCidrDescription": "Eksponer et CIDR-område på sitenettet mod tilkoblede klienter",
|
||||||
|
"privateResourceTypeHttpDescription": "Adgang til en HTTP eller HTTPS service gennem et domæne",
|
||||||
|
"privateResourceTypeSshDescription": "Adgang til en SSH-server fra tilkoblede klienter",
|
||||||
"resourceDomainDescription": "Ressourcen vil blive serveret på dette fuldstændigt kvalificerede domænenavnet.",
|
"resourceDomainDescription": "Ressourcen vil blive serveret på dette fuldstændigt kvalificerede domænenavnet.",
|
||||||
"resourceHTTPSSettings": "HTTPS-indstillinger",
|
"resourceHTTPSSettings": "HTTPS-indstillinger",
|
||||||
"resourceHTTPSSettingsDescription": "Konfigurer hvordan ressourcen skal nås over HTTPS",
|
"resourceHTTPSSettingsDescription": "Konfigurer hvordan ressourcen skal nås over HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Gem indstillinger",
|
"authMethodsSave": "Gem indstillinger",
|
||||||
"policyAuthStackTitle": "Autentificering",
|
"policyAuthStackTitle": "Autentificering",
|
||||||
"policyAuthStackDescription": "Kontroller hvilke autentificeringsmetoder som kræves for at få adgang til denne ressource",
|
"policyAuthStackDescription": "Kontroller hvilke autentificeringsmetoder som kræves for at få adgang til denne ressource",
|
||||||
|
"policyAuthInferenceStackDescription": "Vælg hvilke brugere og roller der kan autentificere til denne AI-gateway",
|
||||||
"policyAuthOrLogicTitle": "Flere autentificeringsmetoder aktive",
|
"policyAuthOrLogicTitle": "Flere autentificeringsmetoder aktive",
|
||||||
"policyAuthOrLogicBanner": "Besøgende kan autentificere sig med en hvilken som helst af de aktive metoder nedenfor. De behøver ikke at fuldføre dem alle.",
|
"policyAuthOrLogicBanner": "Besøgende kan autentificere sig med en hvilken som helst af de aktive metoder nedenfor. De behøver ikke at fuldføre dem alle.",
|
||||||
"policyAuthMethodActive": "Aktiv",
|
"policyAuthMethodActive": "Aktiv",
|
||||||
"policyAuthMethodOff": "Af",
|
"policyAuthMethodOff": "Af",
|
||||||
"policyAuthSsoTitle": "Platform-SSO",
|
"policyAuthSsoTitle": "Platform-SSO",
|
||||||
"policyAuthSsoDescription": "Kræv påloging gennem din organisations identitetsudbyder",
|
"policyAuthSsoDescription": "Kræv påloging gennem din organisations identitetsudbyder",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "Log-in URL",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Hver bruger har allerede en identitets-API-nøgle, du behøver kun at oprette virtuelle API-nøgler til ikke-brugerklienter eller delt adgang. Brugere kan hente deres nøgle ved at logge ind med deres identitetsudbyder på denne ressourcens URL, hvor den vil blive vist efter login.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Hver bruger har allerede en identitets-API-nøgle, du behøver kun at oprette virtuelle API-nøgler til ikke-brugerklienter eller delt adgang. Brugere henter deres nøgle her efter at have logget ind med deres identitetsudbyder.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} brugere, {roles} roller",
|
"policyAuthSsoSummary": "{idp} · {users} brugere, {roles} roller",
|
||||||
"policyAuthSsoDefaultIdp": "Standardudbyder",
|
"policyAuthSsoDefaultIdp": "Standardudbyder",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Tilføj standardidentitetsudbyder",
|
"policyAuthAddDefaultIdentityProvider": "Tilføj standardidentitetsudbyder",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Ny rolle kræves",
|
"accessRoleErrorNewRequired": "Ny rolle kræves",
|
||||||
"accessRoleErrorRemove": "Kunne ikke fjerne rolle",
|
"accessRoleErrorRemove": "Kunne ikke fjerne rolle",
|
||||||
"accessRoleErrorRemoveDescription": "Det opstod en fejl under fjernelse af rollen.",
|
"accessRoleErrorRemoveDescription": "Det opstod en fejl under fjernelse af rollen.",
|
||||||
|
"accessRoleInferenceBudget": "AI-budget",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Konfigurer hvordan medlemmer af denne rolle begrænser AI-brug baseret på forbrug eller token-grænser",
|
||||||
"accessRoleName": "Rollenavn",
|
"accessRoleName": "Rollenavn",
|
||||||
"accessRoleQuestionRemove": "Du er ved at slette rollen `{name}`. Du kan ikke fortryde denne handling.",
|
"accessRoleQuestionRemove": "Du er ved at slette rollen `{name}`. Du kan ikke fortryde denne handling.",
|
||||||
"accessRoleRemove": "Fjern Rolle",
|
"accessRoleRemove": "Fjern Rolle",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Slet site",
|
"actionDeleteSite": "Slet site",
|
||||||
"actionGetSite": "Hent site",
|
"actionGetSite": "Hent site",
|
||||||
"actionListSites": "Vis sites",
|
"actionListSites": "Vis sites",
|
||||||
|
"actionCreateAiProvider": "Opret AI-udbyder",
|
||||||
|
"actionDeleteAiProvider": "Slet AI-udbyder",
|
||||||
|
"actionGetAiProvider": "Få AI-udbyder",
|
||||||
|
"actionListAiProviders": "Vis AI-udbydere",
|
||||||
|
"actionUpdateAiProvider": "Opdater AI-udbyder",
|
||||||
|
"actionCreateAiModel": "Opret AI-model",
|
||||||
|
"actionDeleteAiModel": "Slet AI-model",
|
||||||
|
"actionGetAiModel": "Get AI Model",
|
||||||
|
"actionListAiModels": "Vis AI-modeller",
|
||||||
|
"actionUpdateAiModel": "Opdater AI-model",
|
||||||
|
"actionCreateAiBudget": "Opret AI Budget",
|
||||||
|
"actionDeleteAiBudget": "Slet AI Budget",
|
||||||
|
"actionGetAiBudget": "Get AI Budget",
|
||||||
|
"actionListAiBudgets": "Vis AI Budgetter",
|
||||||
|
"actionUpdateAiBudget": "Opdater AI Budget",
|
||||||
|
"actionListResourceAiModels": "Vis Resource AI Modeller",
|
||||||
|
"actionSetResourceAiModels": "Angiv Resource AI Modeller",
|
||||||
|
"actionCreateVirtualApiKey": "Opret Virtuel API-nøgle",
|
||||||
|
"actionDeleteVirtualApiKey": "Slet Virtuel API-nøgle",
|
||||||
|
"actionGetVirtualApiKey": "Get Virtual API Key",
|
||||||
|
"actionListVirtualApiKeys": "Vis Virtuelle API-nøgler",
|
||||||
|
"actionUpdateVirtualApiKey": "Opdater Virtuel API-nøgle",
|
||||||
"actionApplyBlueprint": "Brug blueprint",
|
"actionApplyBlueprint": "Brug blueprint",
|
||||||
"actionListBlueprints": "Vis blueprints",
|
"actionListBlueprints": "Vis blueprints",
|
||||||
"actionGetBlueprint": "Hent blueprint",
|
"actionGetBlueprint": "Hent blueprint",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Angiv ressource-PIN-kode",
|
"actionSetResourcePincode": "Angiv ressource-PIN-kode",
|
||||||
"actionSetResourceEmailWhitelist": "Angiv e-mailwhitelist for ressource",
|
"actionSetResourceEmailWhitelist": "Angiv e-mailwhitelist for ressource",
|
||||||
"actionGetResourceEmailWhitelist": "Hent e-mailwhitelist for ressource",
|
"actionGetResourceEmailWhitelist": "Hent e-mailwhitelist for ressource",
|
||||||
|
"actionListResourcePolicies": "Vis ressourcepolitikker",
|
||||||
|
"actionCreateResourcePolicy": "Opret ressourcepolitik",
|
||||||
"actionGetResourcePolicy": "Få ressourceretningslinjen",
|
"actionGetResourcePolicy": "Få ressourceretningslinjen",
|
||||||
"actionUpdateResourcePolicy": "Opdater ressourceretningslinjen",
|
"actionUpdateResourcePolicy": "Opdater ressourceretningslinjen",
|
||||||
|
"actionDeleteResourcePolicy": "Slet ressourcepolitik",
|
||||||
"actionSetResourcePolicyUsers": "Angiv ressourceretningslinjens brugere",
|
"actionSetResourcePolicyUsers": "Angiv ressourceretningslinjens brugere",
|
||||||
"actionSetResourcePolicyRoles": "Angiv ressourceretningslinjens roller",
|
"actionSetResourcePolicyRoles": "Angiv ressourceretningslinjens roller",
|
||||||
"actionSetResourcePolicyPassword": "Angiv ressourceretningslinjen for adgangskode",
|
"actionSetResourcePolicyPassword": "Angiv ressourceretningslinjen for adgangskode",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Invitationer",
|
"sidebarInvitations": "Invitationer",
|
||||||
"sidebarRoles": "Roller",
|
"sidebarRoles": "Roller",
|
||||||
"sidebarShareableLinks": "Delbare links",
|
"sidebarShareableLinks": "Delbare links",
|
||||||
|
"sidebarAiGateway": "AI-gateway",
|
||||||
|
"sidebarAiProviders": "Udbydere",
|
||||||
|
"commandAiProviders": "AI-udbydere",
|
||||||
|
"sidebarVirtualApiKeys": "Virtuelle API-nøgler",
|
||||||
|
"sidebarMyApiKeys": "Dine API-nøgler",
|
||||||
|
"sidebarAccount": "Launcher",
|
||||||
|
"commandVirtualApiKeys": "Virtuelle API-nøgler",
|
||||||
|
"virtualApiKeysTitle": "Administrer Virtuelle API-nøgler",
|
||||||
|
"virtualApiKeysDescription": "Opret og administrer manuelle API-nøgler til adgang til AI Gateway for offentlige AI-gateways",
|
||||||
|
"virtualApiKeysTabIdentity": "Identitetsnøgler",
|
||||||
|
"virtualApiKeysTabVirtual": "Virtuelle nøgler",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Identitetsnøgler for hver bruger",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Hver bruger har allerede en Pangolin-identitetsnøgle til denne organisation. Den er unik for deres konto og autentificerer dem til AI-gateways, de kan få adgang til.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Eksempel",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Sådan får brugere deres nøgle",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Besøg en offentlig AI-gateway URL i browseren og log ind med deres konto.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Eller gå til <url></url> mens du er logget ind.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Du kan oprette yderligere virtuelle API-nøgler på Virtuelle nøgler fanen. Disse nøgler kan være omfangsbegrænsede til specifikke offentlige AI-gateways og valgfrit være forbundet med en bruger. Oprettelse af en nøgle giver straks adgang til de valgte offentlige AI-gateways.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Opret en nøgle manuelt",
|
||||||
|
"virtualApiKeysEmailIdentity": "E-maile Identitetsnøgler",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Send hver udvalgt bruger eller rolle deres Pangolin-identitetsnøgle.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Send til alle brugere",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "E-maile hvert organisationmedlem, der har en konto-e-mail.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Brugere",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Roller",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Send e-mails",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Identitetsnøgler er sendt",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Sendte {sent} e-mails.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} brugere blev sprunget over, fordi de ikke har en e-mail adresse.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Vælg mindst en bruger eller rolle, eller send til alle brugere.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Fejl ved sending af identitetsnøgler",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Kunne ikke e-maile identitetsnøgler",
|
||||||
|
"virtualApiKeysBannerTitle": "Identitetsnøgler til hver bruger",
|
||||||
|
"virtualApiKeysBannerDescription": "Hver bruger har allerede en identitetsnøgle tilgængelig på {keysUrl}. Du kan også manuelt generere nøgler her, der giver direkte adgang til offentlige AI-gateways.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Vis Identitetsnøgler",
|
||||||
|
"virtualApiKeys": "Virtuelle API-nøgler",
|
||||||
|
"virtualApiKeysSearch": "Søg nøgler...",
|
||||||
|
"virtualApiKeysCreate": "Opret Virtuel API-nøgle",
|
||||||
|
"virtualApiKeysCreateDescription": "Opret en manuel nøgle, der kan kalde offentlige AI-gateways i denne organisation",
|
||||||
|
"virtualApiKeysCreateButton": "Opret nøgle",
|
||||||
|
"virtualApiKeysEmpty": "Ingen Virtuelle API-nøgler endnu",
|
||||||
|
"virtualApiKeysName": "Navn",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Beskrivelse (valgfri)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Associer Bruger (valgfri)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Associer denne nøgle med en bruger for at spore brugen. Når denne nøgle er oprettet, giver den straks adgang til de valgte offentlige AI-gateways. Du behøver ikke associere en bruger manuelt til ressourcen. Nøglen vil også blive vist i brugerens profil.",
|
||||||
|
"virtualApiKeysAllResources": "Alle offentlige AI-gateways",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Tillad denne nøgle at få adgang til hver offentlig AI-gateway i organisationen",
|
||||||
|
"virtualApiKeysSelectResources": "Offentlige AI Gateways",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Vælg ressourcer",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Vælg hvilke offentlige AI-gateways denne nøgle kan få adgang til",
|
||||||
|
"virtualApiKeysNoResources": "Ingen ressourcer",
|
||||||
|
"virtualApiKeysSecret": "Nøgle",
|
||||||
|
"virtualApiKeysCopyKey": "Kopier denne nøgle. Du kan se den senere igen fra tabellen eller når du redigerer.",
|
||||||
|
"virtualApiKeysViewSecret": "Vis hemmelighed",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Virtuel API Nøglehemmelighed",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Denne hemmelighed giver adgang til de offentlige AI-gateways til denne nøgle",
|
||||||
|
"virtualApiKeysEdit": "Rediger Virtuel API-nøgle",
|
||||||
|
"virtualApiKeysEditDescription": "Opdater den associerede bruger og offentlige AI-gateway adgang for denne nøgle",
|
||||||
|
"virtualApiKeysSaveButton": "Gem ændringer",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Vælg mindst en offentlig AI-gateway, eller aktiver alle offentlige AI-gateways",
|
||||||
|
"virtualApiKeysUpdated": "Virtuel API nøgle opdateret",
|
||||||
|
"virtualApiKeysUpdatedDescription": "Den Virtuelle API nøgle er blevet opdateret",
|
||||||
|
"virtualApiKeysErrorUpdate": "Fejl ved opdatering af virtuel API nøgle",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Kunne ikke opdatere virtuel API nøgle",
|
||||||
|
"virtualApiKeysErrorCreate": "Fejl ved oprettelse af virtuel API nøgle",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Kunne ikke oprette virtuel API nøgle",
|
||||||
|
"virtualApiKeysErrorDelete": "Fejl ved sletning af virtuel API nøgle",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Kunne ikke slette virtuel API nøgle",
|
||||||
|
"virtualApiKeysDeleted": "Virtuel API nøgle slettet",
|
||||||
|
"virtualApiKeysDeletedDescription": "Den Virtuelle API nøgle er blevet slettet",
|
||||||
|
"virtualApiKeysDelete": "Slet Virtuel API-nøgle",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Slet nøgle",
|
||||||
|
"virtualApiKeysQuestionRemove": "Er du sikker på, at du vil slette denne virtuelle API-nøgle?",
|
||||||
|
"virtualApiKeysMessageRemove": "Klienter, der bruger denne nøgle, vil miste adgang med det samme.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Fejl ved indlæsning af hemmelighed",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Kunne ikke indlæse den virtuelle API nøglehemmelighed",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Ikke tildelt",
|
||||||
|
"virtualApiKeysInferenceBudget": "Budget",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Konfigurer hvordan denne nøgle begrænser AI-brug baseret på forbrug eller token-grænser",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Email nøgle ved generering",
|
||||||
|
"virtualApiKeysEmailThisKey": "Email denne nøgle",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Send nøglen til den associerede bruger og yderligere adresser efter oprettelsen",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Send den aktuelle nøgle til den associerede bruger og yderligere adresser",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "Email er ikke konfigureret på denne server",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Konfigurer SMTP for at emaile Virtuelle API-nøgler.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Send til associeret bruger",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Email nøglen til den associerede brugers konto-email",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Associer en bruger for at sende nøglen til den bruger",
|
||||||
|
"virtualApiKeysEmailAdditional": "Yderligere e-mails",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Tilføj email og tryk Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Vælg den associerede bruger eller tilføj mindst én email-adresse",
|
||||||
|
"myVirtualApiKeysTitle": "Dine API-nøgler",
|
||||||
|
"myVirtualApiKeysDescription": "Se din identitetsnøgle og eventuelle Virtuelle API-nøgler tilskrevet dig i denne organisation",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Dine API-nøgler til {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Se din identitetsnøgle og Virtuelle API-nøgler tilskrevet dig, der kan få adgang til {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Identitetsnøgle",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Din Personlige API Nøgle",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Din personlige nøgle til denne organisation. Den er unik for din konto og bruges til at identificere dig ved kald til AI Gateway ressourcer.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Din Personlige API Nøgle til {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Din personlige nøgle til denne organisation. Brug den til at kalde {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Tildelte Nøgler",
|
||||||
|
"myVirtualApiKeysManualDescription": "Manuelle Virtuelle API-nøgler, en admin har tilknyttet din konto",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Manuelle Virtuelle API-nøgler tilknyttet din konto, der kan få adgang til {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Ingen tildelte nøgler endnu",
|
||||||
|
"myVirtualApiKeysKindUser": "Identitet",
|
||||||
|
"myVirtualApiKeysKindManual": "Manuel",
|
||||||
|
"myVirtualApiKeysUnnamed": "Navnløs nøgle",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Vis hemmelighed",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Denne hemmelighed autentificerer dig til AI Gateway ressourcer",
|
||||||
|
"aiProvidersTitle": "AI-udbydere",
|
||||||
|
"aiProvidersDescription": "Forbind modeludbydere til AI-arbejdsopgaver i denne organisation",
|
||||||
|
"aiProvidersBannerTitle": "Forbind Modeludbydere",
|
||||||
|
"aiProvidersBannerDescription": "Udbydere er de moduler, Pangolin bruger til AI-arbejdsopgaver. Forbind OpenAI, Anthropic, og andre udbydere her, og tilknyt dem derefter til offentlige AI-gateways, så brugere kan tilkalde modeller med identitetsbevidst adgang, budgetter, og logning.",
|
||||||
|
"aiProvidersAdd": "Tilføj Udbyder",
|
||||||
|
"aiProvidersSearch": "Søg udbydere...",
|
||||||
|
"aiProvidersEmpty": "Ingen AI-udbydere endnu",
|
||||||
|
"aiProviderCreate": "Opret AI-udbyder",
|
||||||
|
"aiProviderCreateDescription": "Tilføj en modeludbyder for denne organisation",
|
||||||
|
"aiProviderSeeAll": "Se Alle Udbydere",
|
||||||
|
"aiProviderSetting": "Udbyderindstillinger for {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Konfigurer denne AI-udbyder",
|
||||||
|
"aiProviderGeneral": "Generelt",
|
||||||
|
"aiProviderGeneralDescription": "Basisindstillinger for denne udbyder",
|
||||||
|
"aiProviderConfiguration": "Konfiguration",
|
||||||
|
"aiProviderConfigurationDescription": "Netværksrouting og autentificering for denne udbyder",
|
||||||
|
"aiProviderNetworkSettings": "Netværksindstillinger",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Vælg hvordan trafik når denne udbyder",
|
||||||
|
"aiProviderAuthSettings": "Autentificering",
|
||||||
|
"aiProviderAuthSettingsDescription": "Konfigurer hvordan denne udbyder autentificerer anmodninger til sin upstream URL",
|
||||||
|
"aiProviderBudgetSettings": "Budget",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Konfigurer hvordan denne udbyder begrænser brug baseret på forbrug eller token-grænser",
|
||||||
|
"aiBudgetAdd": "Tilføj Budget",
|
||||||
|
"aiBudgetEmpty": "Ingen budgetter konfigureret endnu. Klik på Tilføj Budget for at angive et forbrugs- eller tokengränse.",
|
||||||
|
"aiBudgetUnit": "Forbrugstype",
|
||||||
|
"aiBudgetPeriod": "Nulstil periode",
|
||||||
|
"aiBudgetAmount": "Maksimalt Forbrug",
|
||||||
|
"aiBudgetAmountPlaceholder": "Maksimalt forbrug",
|
||||||
|
"aiBudgetPeriodHourly": "Timelig",
|
||||||
|
"aiBudgetPeriodDaily": "Daglig",
|
||||||
|
"aiBudgetPeriodWeekly": "Ugentlig",
|
||||||
|
"aiBudgetPeriodMonthly": "Månedlig",
|
||||||
|
"aiBudgetPeriodYearly": "Årlig",
|
||||||
|
"aiBudgetPeriodLifetime": "Livstid",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Tokens",
|
||||||
|
"aiBudgetConflictError": "Et budget for denne nulstillingsperiode og forbrugstype eksisterer allerede",
|
||||||
|
"aiBudgetInvalidAmountError": "Indtast et maksimalt forbrug større end 0",
|
||||||
|
"aiBudgetUpdated": "Budgetter opdateret",
|
||||||
|
"aiBudgetErrorSave": "Kunne ikke opdatere budgetter",
|
||||||
|
"aiProviderType": "Udbydertype",
|
||||||
|
"aiProviderTypeSearch": "Søg udbydere...",
|
||||||
|
"aiProviderTypeNotFound": "Ingen udbydertype fundet",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI-gateway",
|
||||||
|
"aiProviderTypeCustom": "Brugerdefineret",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI-gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "Brug din egen endpoint eller ruter via stedmål",
|
||||||
|
"aiProviderUpstreamUrl": "Opstrøms URL",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Basis-URL for udbyderens API",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Lad være tom for at bruge den standard opstrøms URL for denne udbyder",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Effektiv opstrøms URL",
|
||||||
|
"aiProviderApiKey": "API-nøgle",
|
||||||
|
"aiProviderApiKeyDescription": "API-nøgle bruges til at godkende forespørgsler til denne udbyder",
|
||||||
|
"aiProviderCustomHeadersDescription": "Headers sendt ved hver forespørgsel til denne udbyder. Newline adskilt: Header-Name: værdi",
|
||||||
|
"aiProviderApiKeyLastChars": "API-nøgle",
|
||||||
|
"aiProviderAuthType": "Godkendelsestype",
|
||||||
|
"aiProviderAuthTypeSearch": "Søg godkendelsestyper...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Ingen godkendelsestype fundet",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Autorisation: Bearer nøgle. Bruges af OpenAI og de fleste udbydere",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-nøgle",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-nøgle header. Bruges af Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-nøgle",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-nøgle header. Bruges af Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Autorisation: Splunk nøgle. Bruges af Splunk HTTP Event Collector",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-autorisation: Bearer nøgle. Bruges af Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "Ingen godkendelse",
|
||||||
|
"aiProviderAuthTypePassthrough": "Gennemsigtighed",
|
||||||
|
"aiProviderAuthTypeDescription": "Hvordan opstrøms API godkender forespørgsler",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Send opkalderens API-nøgle headers til det opstrøms link",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Send ikke godkendelsesheadere til det opstrøms link",
|
||||||
|
"aiProviderRoutingMode": "Routeringsmodus",
|
||||||
|
"aiProviderRoutingModeDescription": "Send trafik til en opstrøms URL eller til HTTP mål på dine steder",
|
||||||
|
"aiProviderRoutingModeUrl": "Opstrøms URL",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Kald en offentlig eller privat API basis-URL",
|
||||||
|
"aiProviderRoutingModeTarget": "Stedmål",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Rute gennem mål på dine steder",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Efter oprettelse af denne udbyder, konfigurer mål på Netværksindstillinger fanen.",
|
||||||
|
"aiProviderTargetNoOne": "Denne udbyder har ingen mål. Tilføj et mål for at rute forespørgsler gennem dine steder.",
|
||||||
|
"aiProviderSkipTlsVerification": "Spring TLS-verifikation over",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Deaktiver TLS-certifikat verifikation for opstrømsforbindelsen",
|
||||||
|
"aiProviderBudget": "Budget",
|
||||||
|
"aiProviderBudgetDescription": "Valgfrit udgifts- eller token budget for denne udbyder",
|
||||||
|
"aiProviderBudgetAmount": "Budgetbeløb",
|
||||||
|
"aiProviderBudgetUnit": "Budgetenhed",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Tokens",
|
||||||
|
"aiProviderEnabled": "Aktiveret",
|
||||||
|
"aiProviderEnabledDescription": "Deaktiver helt denne udbyder på tværs af alle ressourcer",
|
||||||
|
"aiProviderErrorCreate": "Kunne ikke oprette AI-udbyder",
|
||||||
|
"aiProviderErrorUpdate": "Kunne ikke opdatere AI-udbyder",
|
||||||
|
"aiProviderErrorDelete": "Kunne ikke slette AI-udbyder",
|
||||||
|
"aiProviderErrorLoad": "Kunne ikke indlæse AI-udbyder",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Indtast en gyldig opstrøms URL",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Opstrøms URL er påkrævet for denne udbyder",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Godkendelsestype er påkrævet",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API-nøgle er påkrævet",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Rute til stedmål er kun tilgængelig for brugerdefinerede udbydere",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Vælg mindst en API-kapabilitet",
|
||||||
|
"aiProviderCapabilities": "API-funktioner",
|
||||||
|
"aiProviderCapabilitiesDescription": "Vælg hvilke API-formater denne udbyder kan håndtere. Kendte udbydere starter med anbefalede standarder.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Vælg hvilke API-formater denne specialudbyder kan håndtere",
|
||||||
|
"aiProviderCapabilitiesSelect": "Vælg funktioner",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Ingen funktioner fundet",
|
||||||
|
"aiProviderCapabilitiesSearch": "Søg funktioner...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Chat Kompletteringer",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Understøtter /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI Svar",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Understøtter /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic Beskeder",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Understøtter /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini Generer Indhold",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Understøtter den direkte Gemini API",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Bedrock Modeller Invoker",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Understøtter Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex Generer Indhold",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Understøtter Vertex AI Gemini format",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex Rå Forudsigelse",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Understøtter Vertex AI rawPredict for Anthropic-modeller",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock Samtale",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Understøtter Amazon Bedrock Converse API",
|
||||||
|
"aiProviderCreated": "AI-udbyder oprettet",
|
||||||
|
"aiProviderUpdated": "AI-udbyder opdateret",
|
||||||
|
"aiProviderDeleted": "AI-udbyder slettet",
|
||||||
|
"aiProviderDelete": "Slet udbyder",
|
||||||
|
"aiProviderDeleteConfirm": "Slet udbyder",
|
||||||
|
"aiProviderQuestionRemove": "Er du sikker på, at du vil slette denne AI-udbyder?",
|
||||||
|
"aiProviderMessageRemove": "Dette vil permanent slette udbyderen og dens modeller og mål. Dette kan ikke fortrydes.",
|
||||||
|
"aiProviderErrorNoUpdate": "AI-udbyder er ikke tilgængelig til opdatering",
|
||||||
|
"aiProviderModels": "Modeller",
|
||||||
|
"aiProviderModelsDescription": "Definer tillad og blokér lister for denne udbyder. Forespørgsler skal matche en tillad post og må ikke matche en blokér post.",
|
||||||
|
"aiProviderCreateModelsDescription": "Vælg hvilke modeller denne udbyder kan betjene. En tom tillad-liste nægter al trafik. Du kan tilføje eller ændre modeller senere.",
|
||||||
|
"aiProviderModelsPlaceholder": "Søg modeller eller skriv en brugerdefineret nøgle",
|
||||||
|
"aiProviderModelsAllow": "Tillad Liste",
|
||||||
|
"aiProviderModelsAllowDescription": "Modeller, der kan bruges gennem denne udbyder. Tom betyder, at alt afvises.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Indtast modelnøgle",
|
||||||
|
"aiProviderModelsAllowEmpty": "Ingen modeller tilføjet endnu.",
|
||||||
|
"aiProviderModelsBlock": "Blokér Liste",
|
||||||
|
"aiProviderModelsBlockDescription": "Modeller at afvise, selvom de matcher en tillad post.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Indtast modelnøgle",
|
||||||
|
"aiProviderModelsBlockEmpty": "Ingen modeller tilføjet endnu.",
|
||||||
|
"aiProviderModelsAdd": "Tilføj Modeller",
|
||||||
|
"aiProviderModelsClearAll": "Ryd All",
|
||||||
|
"aiProviderModelsAddCustom": "Tilføj \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Tryk på Enter for at tilføje denne brugerdefinerede modelnøgle.",
|
||||||
|
"aiProviderModelsAddBulk": "Tilføj {count} brugerdefinerede nøgler",
|
||||||
|
"aiProviderModelsAddBulkHint": "Tryk på Enter for at tilføje {count} brugerdefinerede nøgler.",
|
||||||
|
"aiProviderModelsAddOne": "Tilføj {key} nu",
|
||||||
|
"aiProviderModelsAddSelected": "Tilføj Valgte",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} valgt",
|
||||||
|
"aiProviderModelsSelectAll": "Vælg alle",
|
||||||
|
"aiProviderModelsClearSelected": "Ryd",
|
||||||
|
"aiProviderModelsBulkHint": "Vælg kendte modeller eller skriv en brugerdefineret nøgle.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Ingen matchende katalogmodeller.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Kendte Modeller",
|
||||||
|
"aiProviderModelsAllLabel": "Alle modeller",
|
||||||
|
"aiProviderModelsAllPatternHint": "Wildcard: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Tillad alle modeller",
|
||||||
|
"aiProviderModelsAddAllBlock": "Blokér alle modeller",
|
||||||
|
"aiProviderModelsAddAllDescription": "Bruger * wildcard så hver modelnøgle matcher.",
|
||||||
|
"aiProviderModelsViewMore": "Se mere ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Se mindre",
|
||||||
|
"aiProviderModelsRemove": "Fjern model",
|
||||||
|
"aiProviderModelsEditHint": "Klik for at redigere modelindstillinger",
|
||||||
|
"aiProviderModelsSourceCatalog": "Kendt katalog model",
|
||||||
|
"aiProviderModelsSourceCustom": "Brugerdefineret modelnøgle",
|
||||||
|
"aiProviderModelsSourcePattern": "Wildcard mønster",
|
||||||
|
"aiProviderModelsSourceAll": "Matcher hver modelnøgle",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Budget konfigureret",
|
||||||
|
"aiProviderModelsEditTitle": "Rediger Model",
|
||||||
|
"aiProviderModelsEditDescription": "Opdater modellenøgle eller konfigurer dens budget.",
|
||||||
|
"aiProviderModelsBudgetTab": "Budget",
|
||||||
|
"aiProviderModelsKeyLabel": "Modelnøgle",
|
||||||
|
"aiProviderModelsKeyRequired": "Indtast en modelnøgle",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Denne modelnøgle er allerede på en liste",
|
||||||
|
"aiProviderModelsOverlapError": "Disse mønstre kan ikke være på begge lister: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modeller opdateret",
|
||||||
|
"aiProviderModelsErrorUpdate": "Kunne ikke opdatere modeller",
|
||||||
|
"aiResourceProviders": "Udbydere",
|
||||||
|
"aiResourceProvidersDescription": "Vælg hvilke AI-udbydere denne AI-gateway kan bruge",
|
||||||
|
"aiResourceProvidersHelp": "Vedhæft udbydere og vælg arv (brug hver udbyders lister) eller vælg (vælg en tillad liste for denne ressource). Tillad mønstre, der er i konflikt på tværs af vedhæftede udbydere er ikke tilladt.",
|
||||||
|
"aiResourceProvidersSelect": "Vælg udbydere",
|
||||||
|
"aiResourceProvidersEmpty": "Ingen AI-udbydere fundet",
|
||||||
|
"aiResourceProvidersNoneAttached": "Ingen udbydere tilknyttet endnu.",
|
||||||
|
"aiResourceProvidersAdd": "Tilføj udbyder",
|
||||||
|
"aiResourceProvidersRemove": "Fjern udbyder",
|
||||||
|
"aiResourceProviderToggleEnabled": "Aktivér eller deaktiver denne udbyder på ressourcen",
|
||||||
|
"aiResourceProviderDisabled": "Deaktiveret",
|
||||||
|
"aiResourceProvidersUpdated": "Udbydere opdateret",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Kunne ikke opdatere udbydere",
|
||||||
|
"aiResourceProviderEditDescription": "Vælg, hvordan denne udbyders modeller vises på denne ressource.",
|
||||||
|
"viewProviderSettings": "Se Udbyderindstillinger",
|
||||||
|
"aiResourceProviderMode": "Adgangsmodus",
|
||||||
|
"aiResourceProviderModeInherit": "Arv",
|
||||||
|
"aiResourceProviderModeSelect": "Vælg",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Vælg · {count} modeller",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Brug denne udbyders tillad- og blokér lister som konfigureret på udbyderen.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Vælg et underindstillet af denne udbyders tillad liste modeller til denne ressource.",
|
||||||
|
"aiResourceProviderAllowModels": "Tillad liste",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Vælg modeller",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Søg modeller...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Ingen modeller fundet",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Kun modeller fra denne udbyders tillad-liste kan vælges.",
|
||||||
|
"aiResourceAliasRequired": "Alias kræves for AI-gateways",
|
||||||
|
"aiResourceDomainConfiguration": "Domænekonfiguration",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Vælg det domæne, klienter vil bruge for at nå denne AI-gateway.",
|
||||||
|
"aiUsageAnalyticsTitle": "AI Brug Analyse",
|
||||||
|
"aiUsageAnalyticsDescription": "Analyser AI-gateway omkostninger, token forbrug, og aktivitet på tværs af udbydere, ressourcer, roller og brugere",
|
||||||
|
"aiUsageTabOverview": "Oversigt",
|
||||||
|
"aiUsageTabProviders": "Udbydere",
|
||||||
|
"aiUsageTabResources": "Ressourcer",
|
||||||
|
"aiUsageFilterProvider": "Udbyder",
|
||||||
|
"aiUsageFilterModel": "Model",
|
||||||
|
"aiUsageFilterResource": "Ressource",
|
||||||
|
"aiUsageFilterRole": "Rolle",
|
||||||
|
"aiUsageFilterUser": "Bruger",
|
||||||
|
"aiUsageFilterAllProviders": "Alle Udbydere",
|
||||||
|
"aiUsageFilterAllModels": "Alle Modeller",
|
||||||
|
"aiUsageFilterAllResources": "Alle Ressourcer",
|
||||||
|
"aiUsageFilterAllRoles": "Alle Roller",
|
||||||
|
"aiUsageFilterAllUsers": "Alle Brugere",
|
||||||
|
"aiUsageFilterSearch": "Søg...",
|
||||||
|
"aiUsageFilterNotFound": "Ingen valg fundet",
|
||||||
|
"aiUsageResetFilters": "Nulstil Filtre",
|
||||||
|
"aiUsageRefresh": "Opdater",
|
||||||
|
"aiUsageTokenTypePrompt": "Prompt",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Cache læs",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Cache skriv",
|
||||||
|
"aiUsageTokenTypeCompletion": "Komplettering",
|
||||||
|
"aiUsageTokenTypeReasoning": "Begrundelse",
|
||||||
|
"aiUsageRequests": "Forespørgsler",
|
||||||
|
"aiUsageCost": "Omkostning",
|
||||||
|
"aiUsageTokens": "Tokens",
|
||||||
|
"aiUsageOther": "Andet",
|
||||||
|
"aiUsageTotalRequests": "Total Forespørgsler",
|
||||||
|
"aiUsageTotalTokens": "Samlede tokens",
|
||||||
|
"aiUsageTotalCost": "Total Omkostning",
|
||||||
|
"aiUsageEstimated": "Estimeret",
|
||||||
|
"aiUsageRequestVolume": "Forespørgselsvolumen",
|
||||||
|
"aiUsageTokenUsage": "Token Forbrug",
|
||||||
|
"aiUsageModelCost": "Model Omkostning",
|
||||||
|
"aiUsageModelTokens": "Model tokens",
|
||||||
|
"aiUsageTopModels": "Top Modeller",
|
||||||
|
"aiUsageTopProviders": "Top Udbydere",
|
||||||
|
"aiUsageProviderCost": "Udbyder Omkostning",
|
||||||
|
"aiUsageProviderTokenUsage": "Udbyder Token Forbrug",
|
||||||
|
"aiUsageTopResources": "Top Ressourcer",
|
||||||
|
"aiUsageResourceCost": "Ressource Omkostning",
|
||||||
|
"aiUsageResourceTokenUsage": "Ressource Token Forbrug",
|
||||||
|
"aiUsageResourceTypePublic": "Offentlig Ressource",
|
||||||
|
"aiUsageResourceTypeSite": "Privat Ressource",
|
||||||
|
"aiUsageNoResource": "Ingen ressource",
|
||||||
|
"aiUsageRolesTab": "Roller",
|
||||||
|
"aiUsageUsersTab": "Brugere",
|
||||||
|
"aiUsageTopRoles": "Top Roller",
|
||||||
|
"aiUsageRoleCost": "Rolle Omkostning",
|
||||||
|
"aiUsageRoleTokenUsage": "Rolle Token Forbrug",
|
||||||
|
"aiUsageTopUsers": "Top Brugere",
|
||||||
|
"aiUsageUserCost": "Bruger Omkostning",
|
||||||
|
"aiUsageUserTokenUsage": "Bruger Token Forbrug",
|
||||||
|
"aiUsageUnknownUser": "Ukendt bruger",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Virtuelle API Nøgler",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Virtuel API Nøgle",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Alle Virtuelle API Nøgler",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Top Virtuelle API Nøgler",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Virtuel API Nøgle Omkostning",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Virtuel API Nøgle Token Forbrug",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Ingen virtuel API nøgle",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Unavngiven nøgle",
|
||||||
|
"aiUsageLoading": "Indlæser...",
|
||||||
|
"aiUsageNoData": "Ingen data",
|
||||||
|
"resourceBudgetSettings": "Budget",
|
||||||
|
"resourceBudgetSettingsDescription": "Konfigurer, hvordan denne AI-gateway begrænser brug baseret på udgifts- eller token-grænser",
|
||||||
"sidebarApiKeys": "API-nøgler",
|
"sidebarApiKeys": "API-nøgler",
|
||||||
"sidebarProvisioning": "Provisionering",
|
"sidebarProvisioning": "Provisionering",
|
||||||
"sidebarSettings": "Indstillinger",
|
"sidebarSettings": "Indstillinger",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Subnet",
|
"subnetPlaceholder": "Subnet",
|
||||||
"addressDescription": "Den interne adressen til klienten. Skal falle inden for organisationens subnet.",
|
"addressDescription": "Den interne adressen til klienten. Skal falle inden for organisationens subnet.",
|
||||||
"selectSites": "Vælg sites",
|
"selectSites": "Vælg sites",
|
||||||
|
"selectResources": "Vælg ressourcer",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# ressource} other {# ressourcer}}",
|
||||||
"selectLabels": "Vælg etiketter",
|
"selectLabels": "Vælg etiketter",
|
||||||
"sitesDescription": "Klienten vil have forbindelse til de valgte områdene",
|
"sitesDescription": "Klienten vil have forbindelse til de valgte områdene",
|
||||||
"clientInstallOlm": "Installer Olm",
|
"clientInstallOlm": "Installer Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Tid er i sekunder",
|
"timeIsInSeconds": "Tid er i sekunder",
|
||||||
"requireDeviceApproval": "Kræv enhedsgodkendelse",
|
"requireDeviceApproval": "Kræv enhedsgodkendelse",
|
||||||
"requireDeviceApprovalDescription": "Brugere med denne rolle skal have nye enheder godkendt af en administrator, før de kan oprette forbindelse og få adgang til ressourcer.",
|
"requireDeviceApprovalDescription": "Brugere med denne rolle skal have nye enheder godkendt af en administrator, før de kan oprette forbindelse og få adgang til ressourcer.",
|
||||||
"sshSettings": "SSH Indstillinger",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI Gateway",
|
||||||
"sshAccess": "SSH-adgang",
|
"sshAccess": "SSH-adgang",
|
||||||
"rdpSettings": "RDP Indstillinger",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "VNC Indstillinger",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH-server",
|
"sshServer": "SSH-server",
|
||||||
"rdpServer": "RDP-server",
|
"rdpServer": "RDP-server",
|
||||||
"vncServer": "VNC-server",
|
"vncServer": "VNC-server",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI Gateway",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Skema",
|
"editInternalResourceDialogScheme": "Skema",
|
||||||
"editInternalResourceDialogEnableSsl": "Aktivér TLS",
|
"editInternalResourceDialogEnableSsl": "Aktivér TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Tillad ICMP-ping",
|
"privateResourceAllowIcmpPing": "Tillad ICMP-ping",
|
||||||
"privateResourceNetworkAccess": "Netværksadgang",
|
"privateResourceNetworkAccess": "Netværksadgang",
|
||||||
"privateResourceNetworkAccessDescription": "Styr TCP/UDP-portadgang og om ICMP-ping er tilladt for denne ressource.",
|
"privateResourceNetworkAccessDescription": "Styr TCP/UDP-portadgang og om ICMP-ping er tilladt for denne ressource.",
|
||||||
"hostSettings": "Værtindstillinger",
|
"hostSettings": "Host",
|
||||||
"cidrSettings": "CIDR-indstillinger",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Ressourceegenskaber",
|
"createInternalResourceDialogResourceProperties": "Ressourceegenskaber",
|
||||||
"createInternalResourceDialogName": "Navn",
|
"createInternalResourceDialogName": "Navn",
|
||||||
"createInternalResourceDialogSite": "Websted",
|
"createInternalResourceDialogSite": "Websted",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI Gateway",
|
||||||
"scheme": "Skema",
|
"scheme": "Skema",
|
||||||
"createInternalResourceDialogScheme": "Skema",
|
"createInternalResourceDialogScheme": "Skema",
|
||||||
"createInternalResourceDialogEnableSsl": "Aktivér TLS",
|
"createInternalResourceDialogEnableSsl": "Aktivér TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Gyldig adgangskode",
|
"validPassword": "Gyldig adgangskode",
|
||||||
"validEmail": "Gyldig e-mail",
|
"validEmail": "Gyldig e-mail",
|
||||||
"validSSO": "Gyldig SSO",
|
"validSSO": "Gyldig SSO",
|
||||||
|
"validVirtualAPIKey": "Gyldig Virtuel API Nøgle",
|
||||||
"view": "Vis",
|
"view": "Vis",
|
||||||
"configManaged": "Konfiguration administrert",
|
"configManaged": "Konfiguration administrert",
|
||||||
"connectedClient": "Tilsluttet klient",
|
"connectedClient": "Tilsluttet klient",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Logopbevaring",
|
"logRetention": "Logopbevaring",
|
||||||
"logRetentionDescription": "Håndter hvor længe ulike typer logs beholdes for denne organisation, eller deaktivér dem",
|
"logRetentionDescription": "Håndter hvor længe ulike typer logs beholdes for denne organisation, eller deaktivér dem",
|
||||||
"requestLogsDescription": "Se detaljerede forespørgselslogs for ressourcer i denne organisation",
|
"requestLogsDescription": "Se detaljerede forespørgselslogs for ressourcer i denne organisation",
|
||||||
|
"aiSessionLogs": "AI Gateway Øktsprotokoller",
|
||||||
|
"aiSessionLogsDescription": "Se prompt og svarudskrifter for AI gateway forespørgsler i denne organisation",
|
||||||
|
"sidebarLogsAi": "Øktsprotokoller",
|
||||||
|
"commandLogsAi": "Øktsprotokoller",
|
||||||
|
"sidebarLogsAiUsage": "Bruganalyse",
|
||||||
|
"commandLogsAiUsage": "Bruganalyse",
|
||||||
|
"provider": "Udbyder",
|
||||||
|
"capability": "Kompetence",
|
||||||
|
"model": "Model",
|
||||||
|
"virtualApiKey": "Virtuel API Nøgle",
|
||||||
|
"noVirtualApiKey": "Ingen virtuel API nøgle",
|
||||||
|
"stream": "Stream",
|
||||||
|
"streaming": "Streaming",
|
||||||
|
"nonStreaming": "Ikke-Streaming",
|
||||||
|
"statusCode": "Statuskode",
|
||||||
|
"aiSessionId": "Økts-ID",
|
||||||
|
"aiSessionRequest": "Forespørgsel",
|
||||||
|
"aiSessionResponse": "Svar",
|
||||||
|
"aiSessionNoData": "Ingen data optaget",
|
||||||
|
"aiSessionCouldNotParse": "(rå, kunne ikke analysere udskrift)",
|
||||||
|
"aiSessionLogTruncated": "Denne økt blev afkortet inden lagring og kan være ufuldstændig.",
|
||||||
|
"aiSessionViewRaw": "Se Rå JSON",
|
||||||
|
"aiSessionViewChat": "Se Chat",
|
||||||
|
"cost": "Omkostning",
|
||||||
|
"estimated": "Estimeret",
|
||||||
|
"tokenUsage": "Token Forbrug",
|
||||||
|
"promptTokens": "Prompt tokens",
|
||||||
|
"cacheReadTokens": "Cache Læs Tokens",
|
||||||
|
"cacheWriteTokens": "Cache Skriv Tokens",
|
||||||
|
"completionTokens": "Komplettering Tokens",
|
||||||
|
"reasoningTokens": "Begrundelse Tokens",
|
||||||
|
"totalTokens": "Samlede tokens",
|
||||||
"requestAnalyticsDescription": "Se detaljeret anmodningsanalyse for ressourcer i denne organisation",
|
"requestAnalyticsDescription": "Se detaljeret anmodningsanalyse for ressourcer i denne organisation",
|
||||||
"logRetentionRequestLabel": "Be om logbevaring",
|
"logRetentionRequestLabel": "Be om logbevaring",
|
||||||
"logRetentionRequestDescription": "Hvor længe du vil beholde forespørgselslogs",
|
"logRetentionRequestDescription": "Hvor længe du vil beholde forespørgselslogs",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Hvor længe handlingen skal gemmes",
|
"logRetentionActionDescription": "Hvor længe handlingen skal gemmes",
|
||||||
"logRetentionConnectionLabel": "Forbindelseslog",
|
"logRetentionConnectionLabel": "Forbindelseslog",
|
||||||
"logRetentionConnectionDescription": "Hvor længe du vil beholde forbindelseslogs",
|
"logRetentionConnectionDescription": "Hvor længe du vil beholde forbindelseslogs",
|
||||||
|
"logRetentionAISessionsLabel": "AI Gateway Økt Log Opbevaring",
|
||||||
|
"logRetentionAISessionsDescription": "Hvor længe at opbevare AI gateway prompt/svar øktprotokoller",
|
||||||
"logRetentionDisabled": "Deaktiveret",
|
"logRetentionDisabled": "Deaktiveret",
|
||||||
"logRetention3Days": "3 dage",
|
"logRetention3Days": "3 dage",
|
||||||
"logRetention7Days": "7 dage",
|
"logRetention7Days": "7 dage",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Kildeadresse",
|
"sourceAddress": "Kildeadresse",
|
||||||
"destinationAddress": "Måladresse ",
|
"destinationAddress": "Måladresse ",
|
||||||
"duration": "Varighed",
|
"duration": "Varighed",
|
||||||
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink>-licens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er påkrævet for at bruge denne funktion. <bookADemoLink>Book en gratis demo eller POC-prøveversion for at lære mere.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition licens</enterpriseEditionLink> er krævet. <bookADemoLink>Book en demo eller prøve</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for at bruge denne funktion. Denne funktion er også tilgængelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book en gratis demo eller POC-prøve for at lære mere.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition licens</enterpriseEditionLink> er krævet. <bookADemoLink>Book en demo eller prøve</bookADemoLink>",
|
||||||
"certResolver": "Certifikatløser",
|
"certResolver": "Certifikatløser",
|
||||||
"certResolverDescription": "Vælg certifikatløser som skal bruges for denne ressource.",
|
"certResolverDescription": "Vælg certifikatløser som skal bruges for denne ressource.",
|
||||||
"selectCertResolver": "Vælg certifikatløser",
|
"selectCertResolver": "Vælg certifikatløser",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Valg af flere sites muliggør robust routing og failover for høj tilgængelighed.",
|
"internalResourceFormMultiSiteRoutingHelp": "Valg af flere sites muliggør robust routing og failover for høj tilgængelighed.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Læs mere",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Læs mere",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Begrens adgang til specifikke TCP/UDP-porte eller tillade/blokere alle porte.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Begrens adgang til specifikke TCP/UDP-porte eller tillade/blokere alle porte.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "HTTP-konfiguration",
|
"createInternalResourceDialogHttpConfiguration": "Domænekonfiguration",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Vælg domænet klienter vil bruge for at nu denne ressource via HTTP eller HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Vælg domænet klienter vil bruge for at nu denne ressource via HTTP eller HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "HTTP-konfiguration",
|
"editInternalResourceDialogHttpConfiguration": "Domænekonfiguration",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Vælg domænet klienter vil bruge for at nu denne ressource via HTTP eller HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Vælg domænet klienter vil bruge for at nu denne ressource via HTTP eller HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Uden Etiket",
|
"resourceLauncherUnlabeled": "Uden Etiket",
|
||||||
|
"resourceLauncherAiGateway": "AI Gateway",
|
||||||
"resourceLauncherNoSite": "Ingen Site",
|
"resourceLauncherNoSite": "Ingen Site",
|
||||||
|
"resourceLauncherAvailableModels": "Tilgængelige Modeller",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modeller, du kan bruge med denne AI-gateway.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Ingen modeller er tilgængelige for denne ressource.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Kunne ikke indlæse tilgængelige modeller.",
|
||||||
|
"resourceLauncherApiKeys": "API Nøgler",
|
||||||
|
"resourceLauncherApiKeysDescription": "Brug din identitetsnøgle eller en attributeret nøgle til at godkende med denne ressource.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Identitetsnøgle",
|
||||||
|
"resourceLauncherApiKeysManual": "Attributerede Nøgler",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Ingen API-nøgler er tilgængelige for denne ressource.",
|
||||||
|
"resourceLauncherApiKeysError": "Kunne ikke indlæse API-nøgler.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Ingen ressourcer i denne gruppe",
|
"resourceLauncherNoResourcesInGroup": "Ingen ressourcer i denne gruppe",
|
||||||
"resourceLauncherEmptyStateTitle": "Ingen ressourcer tilgængelige",
|
"resourceLauncherEmptyStateTitle": "Ingen ressourcer tilgængelige",
|
||||||
"resourceLauncherEmptyStateDescription": "Du har endnu ikke adgang til nogen ressourcer. Kontakt din administrator for at anmode om adgang.",
|
"resourceLauncherEmptyStateDescription": "Du har endnu ikke adgang til nogen ressourcer. Kontakt din administrator for at anmode om adgang.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Kan ikke slette launcher-visningen. Prøv venligst igen.",
|
"resourceLauncherViewDeleteFailedDescription": "Kan ikke slette launcher-visningen. Prøv venligst igen.",
|
||||||
"memberPortalPrevious": "Forrige",
|
"memberPortalPrevious": "Forrige",
|
||||||
"memberPortalNext": "Næste",
|
"memberPortalNext": "Næste",
|
||||||
"httpSettings": "HTTP Indstillinger",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "TCP Indstillinger",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "UDP Indstillinger",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Oprette en sikker forbindelse…",
|
"sshConnectingDescription": "Oprette en sikker forbindelse…",
|
||||||
"sshConnecting": "Opretter forbindelse til…",
|
"sshConnecting": "Opretter forbindelse til…",
|
||||||
|
|||||||
+512
-16
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Bitte stelle sicher, dass du als korrekter Benutzer angemeldet bist.",
|
"inviteLoginUser": "Bitte stelle sicher, dass du als korrekter Benutzer angemeldet bist.",
|
||||||
"inviteErrorNoUser": "Es tut uns leid, aber es sieht so aus, als sei die Einladung, auf die du zugreifen möchtest, nicht für einen existierenden Benutzer bestimmt.",
|
"inviteErrorNoUser": "Es tut uns leid, aber es sieht so aus, als sei die Einladung, auf die du zugreifen möchtest, nicht für einen existierenden Benutzer bestimmt.",
|
||||||
"inviteCreateUser": "Bitte erstelle zuerst ein Konto.",
|
"inviteCreateUser": "Bitte erstelle zuerst ein Konto.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Einladungen können nur von internen Konten angenommen werden. Melden Sie sich ab und melden Sie sich mit Ihrem Passwort für diese E-Mail an.",
|
||||||
|
"inviteLoginInternalOnly": "Einladungen erfordern ein internes Konto mit einem Passwort. Erstellen Sie ein Konto oder melden Sie sich mit Ihrem Passwort an.",
|
||||||
"goHome": "Zur Startseite",
|
"goHome": "Zur Startseite",
|
||||||
"inviteLogInOtherUser": "Als anderer Benutzer anmelden",
|
"inviteLogInOtherUser": "Als anderer Benutzer anmelden",
|
||||||
"createAnAccount": "Konto erstellen",
|
"createAnAccount": "Konto erstellen",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Kein Land gefunden.",
|
"noCountryFound": "Kein Land gefunden.",
|
||||||
"siteSelectionDescription": "Dieser Standort wird die Verbindung zum Ziel herstellen.",
|
"siteSelectionDescription": "Dieser Standort wird die Verbindung zum Ziel herstellen.",
|
||||||
"resourceType": "Ressourcentyp",
|
"resourceType": "Ressourcentyp",
|
||||||
"resourceTypeDescription": "Dies steuert das Ressourcenprotokoll und wie es im Browser gerendert wird. Dies kann später nicht geändert werden.",
|
"resourceTypeDescription": "Dies steuert das Ressourcenprotokoll und kann später nicht mehr geändert werden.",
|
||||||
|
"resourceTypeSearch": "Suche Ressourcentypen...",
|
||||||
|
"resourceTypeNotFound": "Kein Ressourcentyp gefunden",
|
||||||
|
"resourceTypeHttpDescription": "Leiten Sie Webverkehr über HTTPS unter Verwendung eines Domänennamens weiter",
|
||||||
|
"resourceTypeInferenceDescription": "Leiten Sie KI-Anfragen über angehängte Anbieter weiter",
|
||||||
|
"resourceTypeSshDescription": "Greifen Sie von einem Browser aus auf einen SSH-Server zu",
|
||||||
|
"resourceTypeRdpDescription": "Greifen Sie von einem Browser aus auf einen Remote-Desktop zu",
|
||||||
|
"resourceTypeVncDescription": "Greifen Sie von einem Browser aus auf einen VNC-Desktop zu",
|
||||||
|
"resourceTypeTcpDescription": "Leiten Sie rohe TCP-Datenverkehr durch eine Portnummer weiter",
|
||||||
|
"resourceTypeUdpDescription": "Leiten Sie rohe UDP-Datenverkehr durch eine Portnummer weiter",
|
||||||
|
"privateResourceTypeDescription": "Dies steuert, wie Klienten auf die Ressource zugreifen können. Dies kann später nicht mehr geändert werden.",
|
||||||
|
"privateResourceTypeHostDescription": "Einen einzelnen Host im Netzwerk der Seite für angeschlossene Clients verfügbar machen",
|
||||||
|
"privateResourceTypeCidrDescription": "Einen CIDR-Bereich im Netzwerk der Seite für angeschlossene Clients verfügbar machen",
|
||||||
|
"privateResourceTypeHttpDescription": "Auf einen HTTP- oder HTTPS-Dienst über eine Domain zugreifen",
|
||||||
|
"privateResourceTypeSshDescription": "Auf einen SSH-Server von verbundenen Clients aus zugreifen",
|
||||||
"resourceDomainDescription": "Die Ressource wird unter diesem vollständig qualifizierten Domainnamen bereitgestellt.",
|
"resourceDomainDescription": "Die Ressource wird unter diesem vollständig qualifizierten Domainnamen bereitgestellt.",
|
||||||
"resourceHTTPSSettings": "HTTPS-Einstellungen",
|
"resourceHTTPSSettings": "HTTPS-Einstellungen",
|
||||||
"resourceHTTPSSettingsDescription": "Konfigurieren Sie den Zugriff auf die Ressource über HTTPS",
|
"resourceHTTPSSettingsDescription": "Konfigurieren Sie den Zugriff auf die Ressource über HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Einstellungen speichern",
|
"authMethodsSave": "Einstellungen speichern",
|
||||||
"policyAuthStackTitle": "Authentifizierung",
|
"policyAuthStackTitle": "Authentifizierung",
|
||||||
"policyAuthStackDescription": "Kontrollieren Sie, welche Authentifizierungsmethoden erforderlich sind, um auf diese Ressource zuzugreifen",
|
"policyAuthStackDescription": "Kontrollieren Sie, welche Authentifizierungsmethoden erforderlich sind, um auf diese Ressource zuzugreifen",
|
||||||
|
"policyAuthInferenceStackDescription": "Wählen Sie, welche Benutzer und Rollen sich an diesem KI-Gateway authentifizieren können",
|
||||||
"policyAuthOrLogicTitle": "Mehrere Authentifizierungsmethoden aktiv",
|
"policyAuthOrLogicTitle": "Mehrere Authentifizierungsmethoden aktiv",
|
||||||
"policyAuthOrLogicBanner": "Besucher können sich mit einer der unten aktiven Methoden authentifizieren. Sie müssen nicht alle abschließen.",
|
"policyAuthOrLogicBanner": "Besucher können sich mit einer der unten aktiven Methoden authentifizieren. Sie müssen nicht alle abschließen.",
|
||||||
"policyAuthMethodActive": "Aktiv",
|
"policyAuthMethodActive": "Aktiv",
|
||||||
"policyAuthMethodOff": "Aus",
|
"policyAuthMethodOff": "Aus",
|
||||||
"policyAuthSsoTitle": "Plattform SSO",
|
"policyAuthSsoTitle": "Plattform SSO",
|
||||||
"policyAuthSsoDescription": "Anmeldung über den Identitätsanbieter Ihrer Organisation erforderlich",
|
"policyAuthSsoDescription": "Anmeldung über den Identitätsanbieter Ihrer Organisation erforderlich",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "Anmelde-URL",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Jeder Benutzer hat bereits einen Identitäts-API-Schlüssel, sodass Sie nur virtuelle API-Schlüssel für Nicht-Benutzer-Clients oder gemeinsamen Zugriff erstellen müssen. Benutzer können ihren Schlüssel abrufen, indem sie sich bei Ihrem Identitätsanbieter an der URL dieser Ressource anmelden, wo es nach der Anmeldung angezeigt wird.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Jeder Benutzer hat bereits einen Identitäts-API-Schlüssel, sodass Sie nur virtuelle API-Schlüssel für Nicht-Benutzer-Clients oder gemeinsamen Zugriff erstellen müssen. Benutzer rufen ihren Schlüssel hier ab, nachdem sie sich bei ihrem Identitätsanbieter angemeldet haben.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} Benutzer, {roles} Rollen",
|
"policyAuthSsoSummary": "{idp} · {users} Benutzer, {roles} Rollen",
|
||||||
"policyAuthSsoDefaultIdp": "Standardanbieter",
|
"policyAuthSsoDefaultIdp": "Standardanbieter",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Standardidentitätsanbieter hinzufügen",
|
"policyAuthAddDefaultIdentityProvider": "Standardidentitätsanbieter hinzufügen",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Neue Rolle ist erforderlich",
|
"accessRoleErrorNewRequired": "Neue Rolle ist erforderlich",
|
||||||
"accessRoleErrorRemove": "Fehler beim Entfernen der Rolle",
|
"accessRoleErrorRemove": "Fehler beim Entfernen der Rolle",
|
||||||
"accessRoleErrorRemoveDescription": "Beim Entfernen der Rolle ist ein Fehler aufgetreten.",
|
"accessRoleErrorRemoveDescription": "Beim Entfernen der Rolle ist ein Fehler aufgetreten.",
|
||||||
|
"accessRoleInferenceBudget": "KI-Budget",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Konfigurieren Sie, wie diese Rolle die KI-Nutzung basierend auf Ausgaben oder Tokenlimits einschränkt",
|
||||||
"accessRoleName": "Rollenname",
|
"accessRoleName": "Rollenname",
|
||||||
"accessRoleQuestionRemove": "Du bist dabei die Rolle `{name}` zu löschen. Du kannst diese Aktion nicht rückgängig machen.",
|
"accessRoleQuestionRemove": "Du bist dabei die Rolle `{name}` zu löschen. Du kannst diese Aktion nicht rückgängig machen.",
|
||||||
"accessRoleRemove": "Rolle entfernen",
|
"accessRoleRemove": "Rolle entfernen",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Standort löschen",
|
"actionDeleteSite": "Standort löschen",
|
||||||
"actionGetSite": "Standort abrufen",
|
"actionGetSite": "Standort abrufen",
|
||||||
"actionListSites": "Standorte auflisten",
|
"actionListSites": "Standorte auflisten",
|
||||||
|
"actionCreateAiProvider": "KI-Anbieter erstellen",
|
||||||
|
"actionDeleteAiProvider": "KI-Anbieter löschen",
|
||||||
|
"actionGetAiProvider": "KI-Anbieter abrufen",
|
||||||
|
"actionListAiProviders": "KI-Anbieter auflisten",
|
||||||
|
"actionUpdateAiProvider": "KI-Anbieter aktualisieren",
|
||||||
|
"actionCreateAiModel": "KI-Modell erstellen",
|
||||||
|
"actionDeleteAiModel": "KI-Modell löschen",
|
||||||
|
"actionGetAiModel": "KI-Modell abrufen",
|
||||||
|
"actionListAiModels": "KI-Modelle auflisten",
|
||||||
|
"actionUpdateAiModel": "KI-Modell aktualisieren",
|
||||||
|
"actionCreateAiBudget": "KI-Budget erstellen",
|
||||||
|
"actionDeleteAiBudget": "KI-Budget löschen",
|
||||||
|
"actionGetAiBudget": "KI-Budget abrufen",
|
||||||
|
"actionListAiBudgets": "KI-Budgets auflisten",
|
||||||
|
"actionUpdateAiBudget": "KI-Budget aktualisieren",
|
||||||
|
"actionListResourceAiModels": "Ressourcen-KI-Modelle auflisten",
|
||||||
|
"actionSetResourceAiModels": "Ressourcen-KI-Modelle festlegen",
|
||||||
|
"actionCreateVirtualApiKey": "Virtuellen API-Schlüssel erstellen",
|
||||||
|
"actionDeleteVirtualApiKey": "Virtuellen API-Schlüssel löschen",
|
||||||
|
"actionGetVirtualApiKey": "Virtuellen API-Schlüssel abrufen",
|
||||||
|
"actionListVirtualApiKeys": "Virtuelle API-Schlüssel auflisten",
|
||||||
|
"actionUpdateVirtualApiKey": "Virtuellen API-Schlüssel aktualisieren",
|
||||||
"actionApplyBlueprint": "Blueprint anwenden",
|
"actionApplyBlueprint": "Blueprint anwenden",
|
||||||
"actionListBlueprints": "Blaupausen anzeigen",
|
"actionListBlueprints": "Blaupausen anzeigen",
|
||||||
"actionGetBlueprint": "Erhalte Blaupause",
|
"actionGetBlueprint": "Erhalte Blaupause",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Ressourcen-PIN festlegen",
|
"actionSetResourcePincode": "Ressourcen-PIN festlegen",
|
||||||
"actionSetResourceEmailWhitelist": "Ressourcen-E-Mail-Whitelist festlegen",
|
"actionSetResourceEmailWhitelist": "Ressourcen-E-Mail-Whitelist festlegen",
|
||||||
"actionGetResourceEmailWhitelist": "Ressourcen-E-Mail-Whitelist abrufen",
|
"actionGetResourceEmailWhitelist": "Ressourcen-E-Mail-Whitelist abrufen",
|
||||||
|
"actionListResourcePolicies": "Ressourcen-Richtlinien auflisten",
|
||||||
|
"actionCreateResourcePolicy": "Ressourcen-Richtlinie erstellen",
|
||||||
"actionGetResourcePolicy": "Ressourcenrichtlinie abrufen",
|
"actionGetResourcePolicy": "Ressourcenrichtlinie abrufen",
|
||||||
"actionUpdateResourcePolicy": "Ressourcenrichtlinie aktualisieren",
|
"actionUpdateResourcePolicy": "Ressourcenrichtlinie aktualisieren",
|
||||||
|
"actionDeleteResourcePolicy": "Ressourcen-Richtlinie löschen",
|
||||||
"actionSetResourcePolicyUsers": "Ressourcenrichtlinienbenutzer festlegen",
|
"actionSetResourcePolicyUsers": "Ressourcenrichtlinienbenutzer festlegen",
|
||||||
"actionSetResourcePolicyRoles": "Rollen der Ressourcenrichtlinie festlegen",
|
"actionSetResourcePolicyRoles": "Rollen der Ressourcenrichtlinie festlegen",
|
||||||
"actionSetResourcePolicyPassword": "Passwort der Ressourcenrichtlinie festlegen",
|
"actionSetResourcePolicyPassword": "Passwort der Ressourcenrichtlinie festlegen",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Einladungen",
|
"sidebarInvitations": "Einladungen",
|
||||||
"sidebarRoles": "Rollen",
|
"sidebarRoles": "Rollen",
|
||||||
"sidebarShareableLinks": "Teilbare Links",
|
"sidebarShareableLinks": "Teilbare Links",
|
||||||
|
"sidebarAiGateway": "KI-Gateway",
|
||||||
|
"sidebarAiProviders": "Anbieter",
|
||||||
|
"commandAiProviders": "KI-Anbieter",
|
||||||
|
"sidebarVirtualApiKeys": "Virtuelle API-Schlüssel",
|
||||||
|
"sidebarMyApiKeys": "Ihre API-Schlüssel",
|
||||||
|
"sidebarAccount": "Launcher",
|
||||||
|
"commandVirtualApiKeys": "Virtuelle API-Schlüssel",
|
||||||
|
"virtualApiKeysTitle": "Virtuelle API-Schlüssel verwalten",
|
||||||
|
"virtualApiKeysDescription": "Erstellen und verwalten Sie manuelle API-Schlüssel für den Zugang zum KI-Gateway des öffentlichen KI-Gateways",
|
||||||
|
"virtualApiKeysTabIdentity": "Identitätsschlüssel",
|
||||||
|
"virtualApiKeysTabVirtual": "Virtuelle Schlüssel",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Identitätsschlüssel für jeden Benutzer",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Jeder Benutzer hat bereits einen Pangolin-Identitätsschlüssel für diese Organisation. Es ist einzigartig für ihr Konto und authentifiziert sie bei KI-Gateways, auf die sie zugreifen können.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Beispiel",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Wie Benutzer ihren Schlüssel erhalten",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Besuchen Sie eine URL des öffentlichen KI-Gateways im Browser und melden Sie sich mit ihrem Konto an.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Oder gehen Sie zu <url></url>, während Sie angemeldet sind.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Sie können zusätzliche virtuelle API-Schlüssel auf der Registerkarte \"Virtuelle Schlüssel\" erstellen. Diese Schlüssel können auf bestimmte öffentliche KI-Gateways abgestimmt und optional einem Benutzer zugeordnet werden. Durch das Erstellen eines Schlüssels wird sofortiger Zugang zu den ausgewählten öffentlichen KI-Gateways gewährt.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Schlüssel manuell erstellen",
|
||||||
|
"virtualApiKeysEmailIdentity": "E-Mail-Identitätsschlüssel",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Senden Sie jedem ausgewählten Benutzer oder jeder Rolle ihren Pangolin-Identitätsschlüssel.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "An alle Benutzer senden",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "E-Mail an jedes Mitglied der Organisation, das eine Kontoe-Mail hat.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Benutzergeräte",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Rollen",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "E-Mails senden",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Identitätsschlüssel versandt",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "{sent} E-Mails wurden gesendet.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} Benutzer wurden übersprungen, da sie keine E-Mail-Adresse haben.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Wählen Sie mindestens einen Benutzer oder eine Rolle aus oder senden Sie an alle Benutzer.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Fehler beim Senden der Identitätsschlüssel",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Fehler beim E-Mail-Versand der Identitätsschlüssel",
|
||||||
|
"virtualApiKeysBannerTitle": "Identitätsschlüssel für jeden Benutzer",
|
||||||
|
"virtualApiKeysBannerDescription": "Jeder Benutzer hat bereits einen Identitätsschlüssel verfügbar unter {keysUrl}. Sie können auch manuell Schlüssel generieren, die direkten Zugang zu öffentlichen KI-Gateways gewähren.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Identitätsschlüssel anzeigen",
|
||||||
|
"virtualApiKeys": "Virtuelle API-Schlüssel",
|
||||||
|
"virtualApiKeysSearch": "Schlüssel suchen...",
|
||||||
|
"virtualApiKeysCreate": "Virtuellen API-Schlüssel erstellen",
|
||||||
|
"virtualApiKeysCreateDescription": "Einen manuellen Schlüssel erstellen, der öffentliche KI-Gateways in dieser Organisation aufrufen kann",
|
||||||
|
"virtualApiKeysCreateButton": "Schlüssel erstellen",
|
||||||
|
"virtualApiKeysEmpty": "Noch keine virtuellen API-Schlüssel vorhanden",
|
||||||
|
"virtualApiKeysName": "Name",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Beschreibung (optional)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Benutzer zuordnen (optional)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Ordnen Sie diesen Schlüssel einem Benutzer zu, um die Nutzung zu verfolgen. Sobald erstellt, gewährt dieser Schlüssel sofort Zugriffe auf die ausgewählten öffentlichen KI-Gateways. Es ist nicht notwendig, manuell eine Verbindung zur Ressource herzustellen. Der Schlüssel wird auch im Benutzerprofil angezeigt.",
|
||||||
|
"virtualApiKeysAllResources": "Alle öffentlichen KI-Gateways",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Erlauben Sie diesem Schlüssel, auf jedes öffentliche KI-Gateway in der Organisation zuzugreifen",
|
||||||
|
"virtualApiKeysSelectResources": "Öffentliche KI-Gateways",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Ressourcen auswählen",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Wählen Sie aus, auf welche öffentlichen KI-Gateways dieser Schlüssel zugreifen kann",
|
||||||
|
"virtualApiKeysNoResources": "Keine Ressourcen",
|
||||||
|
"virtualApiKeysSecret": "Schlüssel",
|
||||||
|
"virtualApiKeysCopyKey": "Kopieren Sie diesen Schlüssel. Sie können ihn später aus der Tabelle oder beim Bearbeiten erneut anzeigen.",
|
||||||
|
"virtualApiKeysViewSecret": "Geheimnis anzeigen",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Virtueller API-Schlüsselgeheimnis",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Dieses Geheimnis gewährt Zugang zu den öffentlichen KI-Gateways, die diesem Schlüssel zugewiesen sind",
|
||||||
|
"virtualApiKeysEdit": "Virtuellen API-Schlüssel bearbeiten",
|
||||||
|
"virtualApiKeysEditDescription": "Aktualisieren Sie den zugeordneten Benutzer und den Zugang zu öffentlichen KI-Gateways für diesen Schlüssel",
|
||||||
|
"virtualApiKeysSaveButton": "Änderungen speichern",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Wählen Sie mindestens ein öffentliches KI-Gateway aus oder aktivieren Sie alle öffentlichen KI-Gateways",
|
||||||
|
"virtualApiKeysUpdated": "Virtueller API-Schlüssel aktualisiert",
|
||||||
|
"virtualApiKeysUpdatedDescription": "Der virtuelle API-Schlüssel wurde aktualisiert",
|
||||||
|
"virtualApiKeysErrorUpdate": "Fehler beim Aktualisieren des virtuellen API-Schlüssels",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Fehler beim Aktualisieren des virtuellen API-Schlüssels",
|
||||||
|
"virtualApiKeysErrorCreate": "Fehler beim Erstellen des virtuellen API-Schlüssels",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Fehler beim Erstellen des virtuellen API-Schlüssels",
|
||||||
|
"virtualApiKeysErrorDelete": "Fehler beim Löschen des virtuellen API-Schlüssels",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Fehler beim Löschen des virtuellen API-Schlüssels",
|
||||||
|
"virtualApiKeysDeleted": "Virtueller API-Schlüssel gelöscht",
|
||||||
|
"virtualApiKeysDeletedDescription": "Der virtuelle API-Schlüssel wurde gelöscht",
|
||||||
|
"virtualApiKeysDelete": "Virtuellen API-Schlüssel löschen",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Schlüssel löschen",
|
||||||
|
"virtualApiKeysQuestionRemove": "Sind Sie sicher, dass Sie diesen virtuellen API-Schlüssel löschen möchten?",
|
||||||
|
"virtualApiKeysMessageRemove": "Clients, die diesen Schlüssel verwenden, verlieren sofort den Zugriff.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Fehler beim Laden des Geheimnisses",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Fehler beim Laden des virtuellen API-Schlüsselgeheimnisses",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Unzugewiesen",
|
||||||
|
"virtualApiKeysInferenceBudget": "Budget",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Konfigurieren Sie, wie dieser Schlüssel die KI-Nutzung basierend auf Ausgaben oder Tokenlimits einschränkt",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "E-Mail-Schlüssel bei Erstellung",
|
||||||
|
"virtualApiKeysEmailThisKey": "Diesen Schlüssel per E-Mail senden",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Senden Sie den Schlüssel an den zugeordneten Benutzer und zusätzliche Adressen, nachdem er erstellt wurde",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Senden Sie den aktuellen Schlüssel an den zugeordneten Benutzer und zusätzliche Adressen",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "E-Mail ist auf diesem Server nicht konfiguriert",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Konfigurieren Sie SMTP, um virtuelle API-Schlüssel per E-Mail zu versenden.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "An den zugeordneten Benutzer senden",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Senden Sie den Schlüssel an die Kontoe-Mail des zugeordneten Benutzers",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Ordnen Sie einen Benutzer zu, um den Schlüssel an diesen Benutzer zu senden",
|
||||||
|
"virtualApiKeysEmailAdditional": "Zusätzliche E-Mails",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "E-Mail-Adresse hinzufügen und Enter drücken",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Wählen Sie den zugeordneten Benutzer aus oder fügen Sie mindestens eine E-Mail-Adresse hinzu",
|
||||||
|
"myVirtualApiKeysTitle": "Ihre API-Schlüssel",
|
||||||
|
"myVirtualApiKeysDescription": "Sehen Sie sich Ihren Identitätsschlüssel und alle Ihnen in dieser Organisation zugewiesenen virtuellen API-Schlüssel an",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Ihre API-Schlüssel für {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Sehen Sie sich Ihren Identitätsschlüssel und Ihnen zugewiesene virtuelle API-Schlüssel an, die auf {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Identitätsschlüssel",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Ihr persönlicher API-Schlüssel",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Ihr persönlicher Schlüssel für diese Organisation. Es ist einzigartig für Ihr Konto und wird verwendet, um Sie zu identifizieren, wenn Sie KI-Gateway-Ressourcen aufrufen.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Ihr persönlicher API-Schlüssel für {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Ihr persönlicher Schlüssel für diese Organisation. Verwenden Sie ihn, um {resourceName} anzurufen.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Zugewiesene Schlüssel",
|
||||||
|
"myVirtualApiKeysManualDescription": "Manuelle virtuelle API-Schlüssel, die ein Admin Ihrem Konto zugeordnet hat",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Manuelle virtuelle API-Schlüssel, die Ihrem Konto zugeordnet sind und auf {resourceName} zugreifen können",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Noch keine zugewiesenen Schlüssel vorhanden",
|
||||||
|
"myVirtualApiKeysKindUser": "Identität",
|
||||||
|
"myVirtualApiKeysKindManual": "Manuell",
|
||||||
|
"myVirtualApiKeysUnnamed": "Unbenannter Schlüssel",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Geheimnis offenlegen",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Dieses Geheimnis authentifiziert Sie zu KI-Gateway-Ressourcen",
|
||||||
|
"aiProvidersTitle": "KI-Anbieter",
|
||||||
|
"aiProvidersDescription": "Modellanbieter für KI-Workloads in dieser Organisation verbinden",
|
||||||
|
"aiProvidersBannerTitle": "Modellanbieter verbinden",
|
||||||
|
"aiProvidersBannerDescription": "Anbieter sind die Modell-Backends, die Pangolin für KI-Workloads verwendet. Schließen Sie hier OpenAI, Anthropic und andere Anbieter an und ordnen Sie sie dann öffentlichen KI-Gateways zu, sodass Benutzer Modelle mit identitätsbewusstem Zugang, Budgets und Logging aufrufen können.",
|
||||||
|
"aiProvidersAdd": "Anbieter hinzufügen",
|
||||||
|
"aiProvidersSearch": "Anbieter suchen...",
|
||||||
|
"aiProvidersEmpty": "Noch keine KI-Anbieter vorhanden",
|
||||||
|
"aiProviderCreate": "KI-Anbieter erstellen",
|
||||||
|
"aiProviderCreateDescription": "Fügen Sie einen Modellanbieter für diese Organisation hinzu",
|
||||||
|
"aiProviderSeeAll": "Alle Anbieter anzeigen",
|
||||||
|
"aiProviderSetting": "Anbietereinstellungen für {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Konfigurieren Sie diesen KI-Anbieter",
|
||||||
|
"aiProviderGeneral": "Allgemein",
|
||||||
|
"aiProviderGeneralDescription": "Grundeinstellungen für diesen Anbieter",
|
||||||
|
"aiProviderConfiguration": "Konfiguration",
|
||||||
|
"aiProviderConfigurationDescription": "Netzwerk-Routing und Authentifizierung für diesen Anbieter",
|
||||||
|
"aiProviderNetworkSettings": "Netzwerkeinstellungen",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Wählen Sie aus, wie der Datenverkehr diesen Anbieter erreicht",
|
||||||
|
"aiProviderAuthSettings": "Authentifizierung",
|
||||||
|
"aiProviderAuthSettingsDescription": "Konfigurieren Sie, wie dieser Anbieter Anfragen an seine Upstream-URL authentifiziert",
|
||||||
|
"aiProviderBudgetSettings": "Budget",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Konfigurieren Sie, wie dieser Anbieter die Nutzung basierend auf Ausgabengrenzen oder Tokenlimits einschränkt",
|
||||||
|
"aiBudgetAdd": "Budget hinzufügen",
|
||||||
|
"aiBudgetEmpty": "Noch keine Budgets konfiguriert. Klicken Sie auf \"Budget hinzufügen\", um ein Ausgabe- oder Tokenlimit festzulegen.",
|
||||||
|
"aiBudgetUnit": "Ausgabetyp",
|
||||||
|
"aiBudgetPeriod": "Reset-Zeitraum",
|
||||||
|
"aiBudgetAmount": "Maximaler Verbrauch",
|
||||||
|
"aiBudgetAmountPlaceholder": "Maximaler Verbrauch",
|
||||||
|
"aiBudgetPeriodHourly": "Stündlich",
|
||||||
|
"aiBudgetPeriodDaily": "Täglich",
|
||||||
|
"aiBudgetPeriodWeekly": "Wöchentlich",
|
||||||
|
"aiBudgetPeriodMonthly": "Monatlich",
|
||||||
|
"aiBudgetPeriodYearly": "Jährlich",
|
||||||
|
"aiBudgetPeriodLifetime": "Lebenslang",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Token",
|
||||||
|
"aiBudgetConflictError": "Ein Budget für diesen Reset-Zeitraum und Ausgabetyp existiert bereits",
|
||||||
|
"aiBudgetInvalidAmountError": "Geben Sie einen maximalen Verbrauch größer als 0 ein",
|
||||||
|
"aiBudgetUpdated": "Budgets aktualisiert",
|
||||||
|
"aiBudgetErrorSave": "Fehler beim Aktualisieren der Budgets",
|
||||||
|
"aiProviderType": "Anbietertyp",
|
||||||
|
"aiProviderTypeSearch": "Anbieter suchen...",
|
||||||
|
"aiProviderTypeNotFound": "Kein Anbietertyp gefunden",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "Benutzerdefiniert",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI-API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic-API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "Verwenden Sie Ihren eigenen Endpoint oder leiten Sie über Seitentargets",
|
||||||
|
"aiProviderUpstreamUrl": "Upstream-URL",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Basis-URL für die Provider-API",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Leer lassen, um die Standard-Upstream-URL für diesen Anbieter zu verwenden",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Effektive Upstream-URL",
|
||||||
|
"aiProviderApiKey": "API-Schlüssel",
|
||||||
|
"aiProviderApiKeyDescription": "API-Schlüssel, der zur Authentifizierung von Anfragen an diesen Anbieter verwendet wird",
|
||||||
|
"aiProviderCustomHeadersDescription": "Header werden bei jeder Anfrage an diesen Anbieter gesendet. Neue Zeile getrennt: Header-Name: Wert",
|
||||||
|
"aiProviderApiKeyLastChars": "API-Schlüssel",
|
||||||
|
"aiProviderAuthType": "Auth-Typ",
|
||||||
|
"aiProviderAuthTypeSearch": "Auth-Typen durchsuchen...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Kein Auth-Typ gefunden",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Autorisierung: Bearer-Schlüssel. Wird von OpenAI und den meisten Anbietern verwendet",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key-Header. Wird von Anthropic verwendet",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key-Header. Wird von Google Gemini verwendet",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Autorisierung: Splunk-Schlüssel. Wird vom Splunk HTTP-Ereignissammler verwendet",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer-Schlüssel. Wird vom Cloudflare AI Gateway verwendet",
|
||||||
|
"aiProviderAuthTypeNone": "Keine Auth",
|
||||||
|
"aiProviderAuthTypePassthrough": "Passthrough",
|
||||||
|
"aiProviderAuthTypeDescription": "Wie die Upstream-API Anfragen authentifiziert",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Leiten Sie die API-Schlüssel-Header des Anrufers an den Upstream weiter",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Keine Authentifizierungs-Header an den Upstream senden",
|
||||||
|
"aiProviderRoutingMode": "Routing-Modus",
|
||||||
|
"aiProviderRoutingModeDescription": "Senden Sie Traffic an eine Upstream-URL oder an HTTP-Ziele auf Ihren Sites",
|
||||||
|
"aiProviderRoutingModeUrl": "Upstream-URL",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Rufen Sie eine öffentliche oder private API-Basis-URL auf",
|
||||||
|
"aiProviderRoutingModeTarget": "Site-Ziele",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Über Ziele auf Ihren Sites routen",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Konfigurieren Sie nach der Erstellung dieses Anbieters Site-Ziele auf der Registerkarte 'Netzwerkeinstellungen'.",
|
||||||
|
"aiProviderTargetNoOne": "Dieser Anbieter hat keine Ziele. Fügen Sie ein Ziel hinzu, um Anfragen über Ihre Sites zu leiten.",
|
||||||
|
"aiProviderSkipTlsVerification": "TLS-Überprüfung überspringen",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "TLS-Zertifikatsüberprüfung für die Upstream-Verbindung deaktivieren",
|
||||||
|
"aiProviderBudget": "Budget",
|
||||||
|
"aiProviderBudgetDescription": "Optionales Ausgaben- oder Token-Budget für diesen Anbieter",
|
||||||
|
"aiProviderBudgetAmount": "Budgetbetrag",
|
||||||
|
"aiProviderBudgetUnit": "Budgeteinheit",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Token",
|
||||||
|
"aiProviderEnabled": "Aktiviert",
|
||||||
|
"aiProviderEnabledDescription": "Diesen Anbieter für alle Ressourcen vollständig deaktivieren",
|
||||||
|
"aiProviderErrorCreate": "Fehler beim Erstellen des KI-Anbieters",
|
||||||
|
"aiProviderErrorUpdate": "Fehler beim Aktualisieren des KI-Anbieters",
|
||||||
|
"aiProviderErrorDelete": "Fehler beim Löschen des KI-Anbieters",
|
||||||
|
"aiProviderErrorLoad": "Fehler beim Laden des KI-Anbieters",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Geben Sie eine gültige Upstream-URL ein",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Upstream-URL ist für diesen Anbieter erforderlich",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Auth-Typ ist erforderlich",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API-Schlüssel ist erforderlich",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Routen von Site-Zielen ist nur für benutzerdefinierte Anbieter verfügbar",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Wählen Sie mindestens eine API-Funktion aus",
|
||||||
|
"aiProviderCapabilities": "API-Funktionen",
|
||||||
|
"aiProviderCapabilitiesDescription": "Wählen Sie aus, welche API-Formate dieser Anbieter verarbeiten kann. Bekannte Anbieter beginnen mit empfohlenen Standardeinstellungen.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Wählen Sie aus, welche API-Formate dieser benutzerdefinierte Anbieter verarbeiten kann",
|
||||||
|
"aiProviderCapabilitiesSelect": "Fähigkeiten auswählen",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Keine Fähigkeiten gefunden",
|
||||||
|
"aiProviderCapabilitiesSearch": "Fähigkeiten suchen...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Chat-Abschlüsse",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Unterstützt /v1/chat/Abschlüsse",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI Antworten",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Unterstützt /v1/antworten",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic Nachrichten",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Unterstützt /v1/nachrichten",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini Inhalt erzeugen",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Unterstützt die direkte Gemini-API",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Bedrock Modell Aufruf",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Unterstützt Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex Inhalt erzeugen",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Unterstützt Vertex AI Gemini-Format",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex Rohvorhersage",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Unterstützt Vertex AI rawPredict für Anthropic-Modelle",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock Gespräch",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Unterstützt Amazon Bedrock Converse API",
|
||||||
|
"aiProviderCreated": "KI-Anbieter erstellt",
|
||||||
|
"aiProviderUpdated": "KI-Anbieter aktualisiert",
|
||||||
|
"aiProviderDeleted": "KI-Anbieter gelöscht",
|
||||||
|
"aiProviderDelete": "Anbieter löschen",
|
||||||
|
"aiProviderDeleteConfirm": "Anbieter löschen",
|
||||||
|
"aiProviderQuestionRemove": "Sind Sie sicher, dass Sie diesen KI-Anbieter löschen möchten?",
|
||||||
|
"aiProviderMessageRemove": "Dadurch wird der Anbieter und seine Modelle und Ziele dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.",
|
||||||
|
"aiProviderErrorNoUpdate": "KI-Anbieter ist nicht verfügbar zur Aktualisierung",
|
||||||
|
"aiProviderModels": "Modelle",
|
||||||
|
"aiProviderModelsDescription": "Definieren Sie Zulassungs- und Sperrlisten für diesen Anbieter. Anfragen müssen mit einem erlaubten Eintrag übereinstimmen und dürfen keinen gesperrten Eintrag treffen.",
|
||||||
|
"aiProviderCreateModelsDescription": "Wählen Sie aus, welche Modelle dieser Anbieter bedienen kann. Eine leere Zulassungsliste verweigert den gesamten Datenverkehr. Sie können später Modelle hinzufügen oder ändern.",
|
||||||
|
"aiProviderModelsPlaceholder": "Modelle suchen oder einen benutzerdefinierten Schlüssel eingeben",
|
||||||
|
"aiProviderModelsAllow": "Zulassungsliste",
|
||||||
|
"aiProviderModelsAllowDescription": "Modelle, die durch diesen Anbieter genutzt werden können. Leer bedeutet, alles verweigern.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Modellschlüssel eingeben",
|
||||||
|
"aiProviderModelsAllowEmpty": "Noch keine Modelle hinzugefügt.",
|
||||||
|
"aiProviderModelsBlock": "Sperrliste",
|
||||||
|
"aiProviderModelsBlockDescription": "Modelle, die selbst dann verweigert werden, wenn sie mit einem erlaubten Eintrag übereinstimmen.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Modellschlüssel eingeben",
|
||||||
|
"aiProviderModelsBlockEmpty": "Noch keine Modelle hinzugefügt.",
|
||||||
|
"aiProviderModelsAdd": "Modelle hinzufügen",
|
||||||
|
"aiProviderModelsClearAll": "Alle löschen",
|
||||||
|
"aiProviderModelsAddCustom": "\"{key}\" hinzufügen",
|
||||||
|
"aiProviderModelsAddCustomHint": "Drücken Sie die Eingabetaste, um diesen benutzerdefinierten Modellschlüssel hinzuzufügen.",
|
||||||
|
"aiProviderModelsAddBulk": "{count} benutzerdefinierte Schlüssel hinzufügen",
|
||||||
|
"aiProviderModelsAddBulkHint": "Drücken Sie die Eingabetaste, um {count} benutzerdefinierte Schlüssel hinzuzufügen.",
|
||||||
|
"aiProviderModelsAddOne": "{key} jetzt hinzufügen",
|
||||||
|
"aiProviderModelsAddSelected": "Ausgewählte hinzufügen",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} ausgewählt",
|
||||||
|
"aiProviderModelsSelectAll": "Alle auswählen",
|
||||||
|
"aiProviderModelsClearSelected": "Löschen",
|
||||||
|
"aiProviderModelsBulkHint": "Wählen Sie bekannte Modelle aus oder geben Sie einen benutzerdefinierten Schlüssel ein.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Keine passenden Katalogmodelle.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Bekannte Modelle",
|
||||||
|
"aiProviderModelsAllLabel": "Alle Modelle",
|
||||||
|
"aiProviderModelsAllPatternHint": "Platzhalterzeichen: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Alle Modelle erlauben",
|
||||||
|
"aiProviderModelsAddAllBlock": "Alle Modelle blockieren",
|
||||||
|
"aiProviderModelsAddAllDescription": "Verwendet das Platzhalterzeichen *, sodass jeder Modellschlüssel übereinstimmt.",
|
||||||
|
"aiProviderModelsViewMore": "Mehr anzeigen ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Weniger anzeigen",
|
||||||
|
"aiProviderModelsRemove": "Modell entfernen",
|
||||||
|
"aiProviderModelsEditHint": "Klicken Sie zum Bearbeiten der Modelleinstellungen",
|
||||||
|
"aiProviderModelsSourceCatalog": "Bekannte Katalogmodelle",
|
||||||
|
"aiProviderModelsSourceCustom": "Benutzerdefinierter Modellschlüssel",
|
||||||
|
"aiProviderModelsSourcePattern": "Wildcardschlüssel",
|
||||||
|
"aiProviderModelsSourceAll": "Übereinstimmung mit jedem Modellschlüssel",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Budget konfiguriert",
|
||||||
|
"aiProviderModelsEditTitle": "Modell bearbeiten",
|
||||||
|
"aiProviderModelsEditDescription": "Aktualisieren Sie den Modellschlüssel oder konfigurieren Sie sein Budget.",
|
||||||
|
"aiProviderModelsBudgetTab": "Budget",
|
||||||
|
"aiProviderModelsKeyLabel": "Modellschlüssel",
|
||||||
|
"aiProviderModelsKeyRequired": "Modellschlüssel eingeben",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Dieser Modellschlüssel befindet sich bereits auf einer Liste",
|
||||||
|
"aiProviderModelsOverlapError": "Diese Muster können nicht auf beiden Listen sein: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modelle aktualisiert",
|
||||||
|
"aiProviderModelsErrorUpdate": "Fehler beim Aktualisieren der Modelle",
|
||||||
|
"aiResourceProviders": "Anbieter",
|
||||||
|
"aiResourceProvidersDescription": "Wählen Sie, welche KI-Anbieter dieses KI-Gateway verwenden kann",
|
||||||
|
"aiResourceProvidersHelp": "Fügen Sie Anbieter an und wählen Sie Vererben (Verwendung der Liste des jeweiligen Anbieters) oder auswählen (Wählen Sie eine Zulassungsliste für diese Ressource). Erlaubte Muster, die bei angehängten Anbietern Konflikte verursachen, sind nicht erlaubt.",
|
||||||
|
"aiResourceProvidersSelect": "Anbieter auswählen",
|
||||||
|
"aiResourceProvidersEmpty": "Keine KI-Anbieter gefunden",
|
||||||
|
"aiResourceProvidersNoneAttached": "Noch keine Anbieter angehängt.",
|
||||||
|
"aiResourceProvidersAdd": "Anbieter hinzufügen",
|
||||||
|
"aiResourceProvidersRemove": "Anbieter entfernen",
|
||||||
|
"aiResourceProviderToggleEnabled": "Aktivieren oder Deaktivieren Sie diesen Anbieter auf der Ressource",
|
||||||
|
"aiResourceProviderDisabled": "Deaktiviert",
|
||||||
|
"aiResourceProvidersUpdated": "Anbieter aktualisiert",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Anbieter konnten nicht aktualisiert werden",
|
||||||
|
"aiResourceProviderEditDescription": "Wählen Sie aus, wie die Modelle dieses Anbieters auf dieser Ressource dargestellt werden.",
|
||||||
|
"viewProviderSettings": "Anbietereinstellungen anzeigen",
|
||||||
|
"aiResourceProviderMode": "Zugriffsmodus",
|
||||||
|
"aiResourceProviderModeInherit": "Vererben",
|
||||||
|
"aiResourceProviderModeSelect": "Auswählen",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Auswählen · {count} Modelle",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Verwenden Sie die Zulassungs- und Sperrlisten dieses Anbieters, wie sie am Anbieter konfiguriert sind.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Wählen Sie ein Teilset von Modellen der Zulassungsliste dieses Anbieters für diese Ressource.",
|
||||||
|
"aiResourceProviderAllowModels": "Zulassungsliste",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Modelle auswählen",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Modelle suchen...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Keine Modelle gefunden",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Es können nur Modelle aus der Zulassungsliste dieses Anbieters ausgewählt werden.",
|
||||||
|
"aiResourceAliasRequired": "Alias ist für KI-Gateways erforderlich",
|
||||||
|
"aiResourceDomainConfiguration": "Domain-Konfiguration",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Wählen Sie die Domain aus, die Clients verwenden sollen, um dieses KI-Gateway zu erreichen.",
|
||||||
|
"aiUsageAnalyticsTitle": "KI Nutzungsanalyse",
|
||||||
|
"aiUsageAnalyticsDescription": "Analysieren Sie die Kosten, die Token-Nutzung und die Aktivität des KI-Gateways über Anbieter, Ressourcen, Rollen und Benutzer hinweg",
|
||||||
|
"aiUsageTabOverview": "Überblick",
|
||||||
|
"aiUsageTabProviders": "Anbieter",
|
||||||
|
"aiUsageTabResources": "Ressourcen",
|
||||||
|
"aiUsageFilterProvider": "Anbieter",
|
||||||
|
"aiUsageFilterModel": "Modell",
|
||||||
|
"aiUsageFilterResource": "Ressource",
|
||||||
|
"aiUsageFilterRole": "Rolle",
|
||||||
|
"aiUsageFilterUser": "Benutzer",
|
||||||
|
"aiUsageFilterAllProviders": "Alle Anbieter",
|
||||||
|
"aiUsageFilterAllModels": "Alle Modelle",
|
||||||
|
"aiUsageFilterAllResources": "Alle Ressourcen",
|
||||||
|
"aiUsageFilterAllRoles": "Alle Rollen",
|
||||||
|
"aiUsageFilterAllUsers": "Alle Benutzer",
|
||||||
|
"aiUsageFilterSearch": "Suche...",
|
||||||
|
"aiUsageFilterNotFound": "Keine Optionen gefunden",
|
||||||
|
"aiUsageResetFilters": "Filter zurücksetzen",
|
||||||
|
"aiUsageRefresh": "Aktualisieren",
|
||||||
|
"aiUsageTokenTypePrompt": "Aufforderung",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Cache-Lesezugriff",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Cache-Schreibzugriff",
|
||||||
|
"aiUsageTokenTypeCompletion": "Abschluss",
|
||||||
|
"aiUsageTokenTypeReasoning": "Logik",
|
||||||
|
"aiUsageRequests": "Anfragen",
|
||||||
|
"aiUsageCost": "Kosten",
|
||||||
|
"aiUsageTokens": "Token",
|
||||||
|
"aiUsageOther": "Andere",
|
||||||
|
"aiUsageTotalRequests": "Gesamtanfragen",
|
||||||
|
"aiUsageTotalTokens": "Gesamttoken",
|
||||||
|
"aiUsageTotalCost": "Gesamtkosten",
|
||||||
|
"aiUsageEstimated": "Geschätzt",
|
||||||
|
"aiUsageRequestVolume": "Anfragevolumen",
|
||||||
|
"aiUsageTokenUsage": "Token-Nutzung",
|
||||||
|
"aiUsageModelCost": "Modellkosten",
|
||||||
|
"aiUsageModelTokens": "Modell-Token",
|
||||||
|
"aiUsageTopModels": "Top-Modelle",
|
||||||
|
"aiUsageTopProviders": "Top-Anbieter",
|
||||||
|
"aiUsageProviderCost": "Anbieterkosten",
|
||||||
|
"aiUsageProviderTokenUsage": "Anbieter-Token-Nutzung",
|
||||||
|
"aiUsageTopResources": "Top-Ressourcen",
|
||||||
|
"aiUsageResourceCost": "Ressourcenkosten",
|
||||||
|
"aiUsageResourceTokenUsage": "Ressourcen-Token-Nutzung",
|
||||||
|
"aiUsageResourceTypePublic": "Öffentliche Ressource",
|
||||||
|
"aiUsageResourceTypeSite": "Private Ressource",
|
||||||
|
"aiUsageNoResource": "Keine Ressource",
|
||||||
|
"aiUsageRolesTab": "Rollen",
|
||||||
|
"aiUsageUsersTab": "Benutzer",
|
||||||
|
"aiUsageTopRoles": "Top-Rollen",
|
||||||
|
"aiUsageRoleCost": "Rollen-Kosten",
|
||||||
|
"aiUsageRoleTokenUsage": "Rollen-Token-Nutzung",
|
||||||
|
"aiUsageTopUsers": "Top-Benutzer",
|
||||||
|
"aiUsageUserCost": "Benutzerkosten",
|
||||||
|
"aiUsageUserTokenUsage": "Benutzer-Token-Nutzung",
|
||||||
|
"aiUsageUnknownUser": "Unbekannter Benutzer",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Virtuelle API-Schlüssel",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Virtueller API-Schlüssel",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Alle virtuellen API-Schlüssel",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Top virtuelle API-Schlüssel",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Virtueller API-Schlüssel Kost",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Virtuelle API-Schlüssel Token-Nutzung",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Kein virtueller API-Schlüssel",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Unbenannter Schlüssel",
|
||||||
|
"aiUsageLoading": "Laden...",
|
||||||
|
"aiUsageNoData": "Keine Daten",
|
||||||
|
"resourceBudgetSettings": "Budget",
|
||||||
|
"resourceBudgetSettingsDescription": "Konfigurieren Sie, wie dieses KI-Gateway die Nutzung basierend auf Ausgabe- oder Token-Limits einschränkt",
|
||||||
"sidebarApiKeys": "API-Schlüssel",
|
"sidebarApiKeys": "API-Schlüssel",
|
||||||
"sidebarProvisioning": "Bereitstellung",
|
"sidebarProvisioning": "Bereitstellung",
|
||||||
"sidebarSettings": "Einstellungen",
|
"sidebarSettings": "Einstellungen",
|
||||||
@@ -2012,7 +2457,7 @@
|
|||||||
"billingSites": "Standorte",
|
"billingSites": "Standorte",
|
||||||
"billingUsers": "Benutzergeräte",
|
"billingUsers": "Benutzergeräte",
|
||||||
"billingDomains": "Domänen",
|
"billingDomains": "Domänen",
|
||||||
"billingOrganizations": "Orden",
|
"billingOrganizations": "Organisationen",
|
||||||
"billingRemoteExitNodes": "Entfernte Knoten",
|
"billingRemoteExitNodes": "Entfernte Knoten",
|
||||||
"billingPublicResources": "Öffentliche Ressourcen",
|
"billingPublicResources": "Öffentliche Ressourcen",
|
||||||
"billingPrivateResources": "Private Ressourcen",
|
"billingPrivateResources": "Private Ressourcen",
|
||||||
@@ -2172,7 +2617,7 @@
|
|||||||
"billingResolvePaymentIssue": "Bitte beheben Sie Ihr Zahlungsproblem vor dem Upgrade oder Herabstufen",
|
"billingResolvePaymentIssue": "Bitte beheben Sie Ihr Zahlungsproblem vor dem Upgrade oder Herabstufen",
|
||||||
"signUpTerms": {
|
"signUpTerms": {
|
||||||
"IAgreeToThe": "Ich stimme den",
|
"IAgreeToThe": "Ich stimme den",
|
||||||
"termsOfService": "Nutzungsbedingungen zu",
|
"termsOfService": "Nutzungsbedingungen",
|
||||||
"and": "und",
|
"and": "und",
|
||||||
"privacyPolicy": "datenschutzrichtlinie."
|
"privacyPolicy": "datenschutzrichtlinie."
|
||||||
},
|
},
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Subnetz",
|
"subnetPlaceholder": "Subnetz",
|
||||||
"addressDescription": "Die interne Adresse des Clients. Muss in das Subnetz der Organisation fallen.",
|
"addressDescription": "Die interne Adresse des Clients. Muss in das Subnetz der Organisation fallen.",
|
||||||
"selectSites": "Standorte auswählen",
|
"selectSites": "Standorte auswählen",
|
||||||
|
"selectResources": "Ressourcen auswählen",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# resource} other {# resources}}",
|
||||||
"selectLabels": "Etiketten auswählen",
|
"selectLabels": "Etiketten auswählen",
|
||||||
"sitesDescription": "Der Client wird zu den ausgewählten Standorten eine Verbindung haben.",
|
"sitesDescription": "Der Client wird zu den ausgewählten Standorten eine Verbindung haben.",
|
||||||
"clientInstallOlm": "Olm installieren",
|
"clientInstallOlm": "Olm installieren",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Zeit ist in Sekunden",
|
"timeIsInSeconds": "Zeit ist in Sekunden",
|
||||||
"requireDeviceApproval": "Gerätegenehmigungen erforderlich",
|
"requireDeviceApproval": "Gerätegenehmigungen erforderlich",
|
||||||
"requireDeviceApprovalDescription": "Benutzer mit dieser Rolle benötigen neue Geräte, die von einem Administrator genehmigt wurden, bevor sie sich verbinden und auf Ressourcen zugreifen können.",
|
"requireDeviceApprovalDescription": "Benutzer mit dieser Rolle benötigen neue Geräte, die von einem Administrator genehmigt wurden, bevor sie sich verbinden und auf Ressourcen zugreifen können.",
|
||||||
"sshSettings": "SSH-Einstellungen",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "KI-Gateway",
|
||||||
"sshAccess": "SSH Zugriff",
|
"sshAccess": "SSH Zugriff",
|
||||||
"rdpSettings": "RDP-Einstellungen",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "VNC-Einstellungen",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH-Server",
|
"sshServer": "SSH-Server",
|
||||||
"rdpServer": "RDP-Server",
|
"rdpServer": "RDP-Server",
|
||||||
"vncServer": "VNC-Server",
|
"vncServer": "VNC-Server",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "KI-Gateway",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Schema",
|
"editInternalResourceDialogScheme": "Schema",
|
||||||
"editInternalResourceDialogEnableSsl": "TLS aktivieren",
|
"editInternalResourceDialogEnableSsl": "TLS aktivieren",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "ICMP-Ping zulassen",
|
"privateResourceAllowIcmpPing": "ICMP-Ping zulassen",
|
||||||
"privateResourceNetworkAccess": "Netzwerkzugriff",
|
"privateResourceNetworkAccess": "Netzwerkzugriff",
|
||||||
"privateResourceNetworkAccessDescription": "TCP/UDP-Portzugriff kontrollieren und ICMP-Ping für diese Ressource zulassen.",
|
"privateResourceNetworkAccessDescription": "TCP/UDP-Portzugriff kontrollieren und ICMP-Ping für diese Ressource zulassen.",
|
||||||
"hostSettings": "Host-Einstellungen",
|
"hostSettings": "Host",
|
||||||
"cidrSettings": "CIDR-Einstellungen",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Ressourceneigenschaften",
|
"createInternalResourceDialogResourceProperties": "Ressourceneigenschaften",
|
||||||
"createInternalResourceDialogName": "Name",
|
"createInternalResourceDialogName": "Name",
|
||||||
"createInternalResourceDialogSite": "Standort",
|
"createInternalResourceDialogSite": "Standort",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "KI-Gateway",
|
||||||
"scheme": "Schema",
|
"scheme": "Schema",
|
||||||
"createInternalResourceDialogScheme": "Schema",
|
"createInternalResourceDialogScheme": "Schema",
|
||||||
"createInternalResourceDialogEnableSsl": "TLS aktivieren",
|
"createInternalResourceDialogEnableSsl": "TLS aktivieren",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Gültiges Passwort",
|
"validPassword": "Gültiges Passwort",
|
||||||
"validEmail": "Gültige E-Mail-Adresse",
|
"validEmail": "Gültige E-Mail-Adresse",
|
||||||
"validSSO": "Gültige SSO-Anmeldung",
|
"validSSO": "Gültige SSO-Anmeldung",
|
||||||
|
"validVirtualAPIKey": "Gültiger virtueller API-Schlüssel",
|
||||||
"view": "Ansehen",
|
"view": "Ansehen",
|
||||||
"configManaged": "Konfiguration verwaltet",
|
"configManaged": "Konfiguration verwaltet",
|
||||||
"connectedClient": "Verbundenes Gerät",
|
"connectedClient": "Verbundenes Gerät",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Log-Speicherung",
|
"logRetention": "Log-Speicherung",
|
||||||
"logRetentionDescription": "Verwalten, wie lange verschiedene Logs für diese Organisation gespeichert werden oder deaktivieren",
|
"logRetentionDescription": "Verwalten, wie lange verschiedene Logs für diese Organisation gespeichert werden oder deaktivieren",
|
||||||
"requestLogsDescription": "Detaillierte Request-Logs für Ressourcen in dieser Organisation anzeigen",
|
"requestLogsDescription": "Detaillierte Request-Logs für Ressourcen in dieser Organisation anzeigen",
|
||||||
|
"aiSessionLogs": "AI-Gateway Sitzungsprotokolle",
|
||||||
|
"aiSessionLogsDescription": "Zeigen Sie Aufforderungs- und Antwortprotokolle für Anfragen des KI-Gateways in dieser Organisation an",
|
||||||
|
"sidebarLogsAi": "Sitzungsprotokolle",
|
||||||
|
"commandLogsAi": "Sitzungsprotokolle",
|
||||||
|
"sidebarLogsAiUsage": "Nutzungsanalyse",
|
||||||
|
"commandLogsAiUsage": "Nutzungsanalyse",
|
||||||
|
"provider": "Anbieter",
|
||||||
|
"capability": "Fähigkeit",
|
||||||
|
"model": "Modell",
|
||||||
|
"virtualApiKey": "Virtueller API-Schlüssel",
|
||||||
|
"noVirtualApiKey": "Kein virtueller API-Schlüssel",
|
||||||
|
"stream": "Strom",
|
||||||
|
"streaming": "Streaming",
|
||||||
|
"nonStreaming": "Nicht-Streaming",
|
||||||
|
"statusCode": "Statuscode",
|
||||||
|
"aiSessionId": "Sitzungs-ID",
|
||||||
|
"aiSessionRequest": "Anfrage",
|
||||||
|
"aiSessionResponse": "Antwort",
|
||||||
|
"aiSessionNoData": "Keine Daten erfasst",
|
||||||
|
"aiSessionCouldNotParse": "(Roh, Protokoll konnte nicht geparst werden)",
|
||||||
|
"aiSessionLogTruncated": "Diese Sitzung wurde vor der Speicherung abgeschnitten und könnte unvollständig sein.",
|
||||||
|
"aiSessionViewRaw": "Rohes JSON anzeigen",
|
||||||
|
"aiSessionViewChat": "Chat anzeigen",
|
||||||
|
"cost": "Kosten",
|
||||||
|
"estimated": "Geschätzt",
|
||||||
|
"tokenUsage": "Token-Nutzung",
|
||||||
|
"promptTokens": "Aufforderungstoken",
|
||||||
|
"cacheReadTokens": "Cache-Lese-Token",
|
||||||
|
"cacheWriteTokens": "Cache-Schreib-Token",
|
||||||
|
"completionTokens": "Abschlusstoken",
|
||||||
|
"reasoningTokens": "Logik-Token",
|
||||||
|
"totalTokens": "Gesamttoken",
|
||||||
"requestAnalyticsDescription": "Detaillierte Anfrage-Analyse für Ressourcen in dieser Organisation anzeigen",
|
"requestAnalyticsDescription": "Detaillierte Anfrage-Analyse für Ressourcen in dieser Organisation anzeigen",
|
||||||
"logRetentionRequestLabel": "HTTP Anforderungsprotokoll Aufbewahrung",
|
"logRetentionRequestLabel": "HTTP Anforderungsprotokoll Aufbewahrung",
|
||||||
"logRetentionRequestDescription": "Wie lange sollen Request-Logs gespeichert werden",
|
"logRetentionRequestDescription": "Wie lange sollen Request-Logs gespeichert werden",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Dauer des Action-Logs",
|
"logRetentionActionDescription": "Dauer des Action-Logs",
|
||||||
"logRetentionConnectionLabel": "Verbindungsprotokoll-Speicherung",
|
"logRetentionConnectionLabel": "Verbindungsprotokoll-Speicherung",
|
||||||
"logRetentionConnectionDescription": "Wie lange Verbindungsprotokolle gespeichert werden sollen",
|
"logRetentionConnectionDescription": "Wie lange Verbindungsprotokolle gespeichert werden sollen",
|
||||||
|
"logRetentionAISessionsLabel": "Aufbewahrung von Sitzungsprotokollen des KI-Gateways",
|
||||||
|
"logRetentionAISessionsDescription": "Wie lange Sitzunglogs von Aufforderung / Antwort des KI-Gateways aufbewahrt werden",
|
||||||
"logRetentionDisabled": "Deaktiviert",
|
"logRetentionDisabled": "Deaktiviert",
|
||||||
"logRetention3Days": "3 Tage",
|
"logRetention3Days": "3 Tage",
|
||||||
"logRetention7Days": "7 Tage",
|
"logRetention7Days": "7 Tage",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Quelladresse",
|
"sourceAddress": "Quelladresse",
|
||||||
"destinationAddress": "Zieladresse",
|
"destinationAddress": "Zieladresse",
|
||||||
"duration": "Dauer",
|
"duration": "Dauer",
|
||||||
"licenseRequiredToUse": "Eine <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> Lizenz oder <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> wird benötigt, um diese Funktion zu nutzen. <bookADemoLink>Buchen Sie eine kostenlose Demo oder POC Testversion, um mehr zu erfahren.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition Lizenz</enterpriseEditionLink> ist erforderlich. <bookADemoLink>Einen Test vorführen oder ausprobieren </bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "Die <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> ist notwendig, um diese Funktion zu nutzen. Diese Funktion ist auch in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> verfügbar. <bookADemoLink>Buchen Sie eine kostenlose Demo oder POC Testversion, um mehr zu erfahren.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition Lizenz</enterpriseEditionLink> ist erforderlich. <bookADemoLink>Vereinbare eine Demo oder einen Test</bookADemoLink>",
|
||||||
"certResolver": "Zertifikatsauflöser",
|
"certResolver": "Zertifikatsauflöser",
|
||||||
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
|
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
|
||||||
"selectCertResolver": "Zertifikatsauflöser auswählen",
|
"selectCertResolver": "Zertifikatsauflöser auswählen",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Durch die Auswahl mehrerer Seiten wird ein ausfallsicheres Routing und Failover für hohe Verfügbarkeit ermöglicht.",
|
"internalResourceFormMultiSiteRoutingHelp": "Durch die Auswahl mehrerer Seiten wird ein ausfallsicheres Routing und Failover für hohe Verfügbarkeit ermöglicht.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Mehr erfahren",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Mehr erfahren",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Den Zugriff auf bestimmte TCP/UDP-Ports beschränken oder alle Ports erlauben/blockieren.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Den Zugriff auf bestimmte TCP/UDP-Ports beschränken oder alle Ports erlauben/blockieren.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "HTTP-Konfiguration",
|
"createInternalResourceDialogHttpConfiguration": "Domain-Konfiguration",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Wählen Sie die Domain, die Clients verwenden, um über HTTP oder HTTPS auf diese Ressource zuzugreifen.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Wählen Sie die Domain, die Clients verwenden, um über HTTP oder HTTPS auf diese Ressource zuzugreifen.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "HTTP-Konfiguration",
|
"editInternalResourceDialogHttpConfiguration": "Domain-Konfiguration",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Wählen Sie die Domain, die Clients verwenden, um über HTTP oder HTTPS auf diese Ressource zuzugreifen.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Wählen Sie die Domain, die Clients verwenden, um über HTTP oder HTTPS auf diese Ressource zuzugreifen.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3390,7 +3875,7 @@
|
|||||||
"enterMfaCode": "Geben Sie den Code aus Ihrer Authentifizierungs-App ein",
|
"enterMfaCode": "Geben Sie den Code aus Ihrer Authentifizierungs-App ein",
|
||||||
"securityKeyRequired": "Bitte verwenden Sie Ihren Sicherheitsschlüssel zum Anmelden.",
|
"securityKeyRequired": "Bitte verwenden Sie Ihren Sicherheitsschlüssel zum Anmelden.",
|
||||||
"needToUseAnotherAccount": "Benötigen Sie ein anderes Konto?",
|
"needToUseAnotherAccount": "Benötigen Sie ein anderes Konto?",
|
||||||
"loginLegalDisclaimer": "Indem Sie auf die Buttons unten klicken, bestätigen Sie, dass Sie gelesen haben, verstehen, und stimmen den <termsOfService>Nutzungsbedingungen</termsOfService> und <privacyPolicy>Datenschutzrichtlinien</privacyPolicy> zu.",
|
"loginLegalDisclaimer": "Indem Sie auf die Buttons unten klicken, bestätigen Sie, dass Sie die <termsOfService>Nutzungsbedingungen</termsOfService> und <privacyPolicy>Datenschutzrichtlinien</privacyPolicy> gelesen haben, verstehen und ihnen zustimmen.",
|
||||||
"termsOfService": "Nutzungsbedingungen",
|
"termsOfService": "Nutzungsbedingungen",
|
||||||
"privacyPolicy": "Datenschutzerklärung",
|
"privacyPolicy": "Datenschutzerklärung",
|
||||||
"userNotFoundWithUsername": "Kein Benutzer mit diesem Benutzernamen gefunden.",
|
"userNotFoundWithUsername": "Kein Benutzer mit diesem Benutzernamen gefunden.",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Nicht etikettiert",
|
"resourceLauncherUnlabeled": "Nicht etikettiert",
|
||||||
|
"resourceLauncherAiGateway": "KI-Gateway",
|
||||||
"resourceLauncherNoSite": "Kein Standort",
|
"resourceLauncherNoSite": "Kein Standort",
|
||||||
|
"resourceLauncherAvailableModels": "Verfügbare Modelle",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modelle, die Sie mit diesem KI-Gateway verwenden können.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Für diese Ressource sind keine Modelle verfügbar.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Verfügbare Modelle konnten nicht geladen werden.",
|
||||||
|
"resourceLauncherApiKeys": "API-Schlüssel",
|
||||||
|
"resourceLauncherApiKeysDescription": "Verwenden Sie Ihren Identitätsschlüssel oder einen zugeordneten Schlüssel, um sich bei dieser Ressource zu authentifizieren.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Identitätsschlüssel",
|
||||||
|
"resourceLauncherApiKeysManual": "Zugeordnete Schlüssel",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Für diese Ressource stehen keine API-Schlüssel zur Verfügung.",
|
||||||
|
"resourceLauncherApiKeysError": "API-Schlüssel konnten nicht geladen werden.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Keine Ressourcen in dieser Gruppe",
|
"resourceLauncherNoResourcesInGroup": "Keine Ressourcen in dieser Gruppe",
|
||||||
"resourceLauncherEmptyStateTitle": "Keine Ressourcen verfügbar",
|
"resourceLauncherEmptyStateTitle": "Keine Ressourcen verfügbar",
|
||||||
"resourceLauncherEmptyStateDescription": "Sie haben noch keinen Zugriff auf Ressourcen. Kontaktieren Sie Ihren Administrator, um Zugriff anzufordern.",
|
"resourceLauncherEmptyStateDescription": "Sie haben noch keinen Zugriff auf Ressourcen. Kontaktieren Sie Ihren Administrator, um Zugriff anzufordern.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Die Startansicht konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
"resourceLauncherViewDeleteFailedDescription": "Die Startansicht konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.",
|
||||||
"memberPortalPrevious": "Vorherige",
|
"memberPortalPrevious": "Vorherige",
|
||||||
"memberPortalNext": "Nächste",
|
"memberPortalNext": "Nächste",
|
||||||
"httpSettings": "HTTP-Einstellungen",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "TCP-Einstellungen",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "UDP-Einstellungen",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Aufbau einer sicheren Verbindung…",
|
"sshConnectingDescription": "Aufbau einer sicheren Verbindung…",
|
||||||
"sshConnecting": "Verbindung wird hergestellt…",
|
"sshConnecting": "Verbindung wird hergestellt…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Please make sure you're logged in as the correct user.",
|
"inviteLoginUser": "Please make sure you're logged in as the correct user.",
|
||||||
"inviteErrorNoUser": "We're sorry, but it looks like the invite you're trying to access is not for a user that exists.",
|
"inviteErrorNoUser": "We're sorry, but it looks like the invite you're trying to access is not for a user that exists.",
|
||||||
"inviteCreateUser": "Please create an account first.",
|
"inviteCreateUser": "Please create an account first.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Invites can only be accepted by internal accounts. Sign out and log in with your password for this email.",
|
||||||
|
"inviteLoginInternalOnly": "Invites require an internal account with a password. Create an account or sign in with your password.",
|
||||||
"goHome": "Go Home",
|
"goHome": "Go Home",
|
||||||
"inviteLogInOtherUser": "Log In as a Different User",
|
"inviteLogInOtherUser": "Log In as a Different User",
|
||||||
"createAnAccount": "Create an Account",
|
"createAnAccount": "Create an Account",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "No country found.",
|
"noCountryFound": "No country found.",
|
||||||
"siteSelectionDescription": "This site will provide connectivity to the target.",
|
"siteSelectionDescription": "This site will provide connectivity to the target.",
|
||||||
"resourceType": "Resource Type",
|
"resourceType": "Resource Type",
|
||||||
"resourceTypeDescription": "This controls the resource protocol and how it will be rendered in the browser. This can’t be changed later.",
|
"resourceTypeDescription": "This controls the resource protocol and can’t be changed later.",
|
||||||
|
"resourceTypeSearch": "Search resource types...",
|
||||||
|
"resourceTypeNotFound": "No resource type found",
|
||||||
|
"resourceTypeHttpDescription": "Proxy web traffic over HTTPS using a domain name",
|
||||||
|
"resourceTypeInferenceDescription": "Route AI requests through attached providers",
|
||||||
|
"resourceTypeSshDescription": "Access an SSH server from the browser",
|
||||||
|
"resourceTypeRdpDescription": "Access a remote desktop from the browser",
|
||||||
|
"resourceTypeVncDescription": "Access a VNC desktop from the browser",
|
||||||
|
"resourceTypeTcpDescription": "Proxy raw TCP traffic using a port number",
|
||||||
|
"resourceTypeUdpDescription": "Proxy raw UDP traffic using a port number",
|
||||||
|
"privateResourceTypeDescription": "This controls how clients reach the resource. This can’t be changed later.",
|
||||||
|
"privateResourceTypeHostDescription": "Expose a single host on the site network to connected clients",
|
||||||
|
"privateResourceTypeCidrDescription": "Expose a CIDR range on the site network to connected clients",
|
||||||
|
"privateResourceTypeHttpDescription": "Access an HTTP or HTTPS service through a domain",
|
||||||
|
"privateResourceTypeSshDescription": "Access an SSH server from connected clients",
|
||||||
"resourceDomainDescription": "The resource will be served at this fully qualified domain name.",
|
"resourceDomainDescription": "The resource will be served at this fully qualified domain name.",
|
||||||
"resourceHTTPSSettings": "HTTPS Settings",
|
"resourceHTTPSSettings": "HTTPS Settings",
|
||||||
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
|
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Save Settings",
|
"authMethodsSave": "Save Settings",
|
||||||
"policyAuthStackTitle": "Authentication",
|
"policyAuthStackTitle": "Authentication",
|
||||||
"policyAuthStackDescription": "Control which authentication methods are required to access this resource",
|
"policyAuthStackDescription": "Control which authentication methods are required to access this resource",
|
||||||
|
"policyAuthInferenceStackDescription": "Choose which users and roles can authenticate to this AI gateway",
|
||||||
"policyAuthOrLogicTitle": "Multiple authentication methods active",
|
"policyAuthOrLogicTitle": "Multiple authentication methods active",
|
||||||
"policyAuthOrLogicBanner": "Visitors may authenticate using any one of the active methods below. They do not need to complete all of them.",
|
"policyAuthOrLogicBanner": "Visitors may authenticate using any one of the active methods below. They do not need to complete all of them.",
|
||||||
"policyAuthMethodActive": "Active",
|
"policyAuthMethodActive": "Active",
|
||||||
"policyAuthMethodOff": "Off",
|
"policyAuthMethodOff": "Off",
|
||||||
"policyAuthSsoTitle": "Platform SSO",
|
"policyAuthSsoTitle": "Platform SSO",
|
||||||
"policyAuthSsoDescription": "Require sign-in through your organization's identity provider",
|
"policyAuthSsoDescription": "Require sign-in through your organization's identity provider",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "Sign-in URL",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users can retrieve their key by signing in with their identity provider at this resource's URL, where it will be shown after login.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users retrieve their key here after signing in with their identity provider.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} users, {roles} roles",
|
"policyAuthSsoSummary": "{idp} · {users} users, {roles} roles",
|
||||||
"policyAuthSsoDefaultIdp": "Default provider",
|
"policyAuthSsoDefaultIdp": "Default provider",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Add Default Identity Provider",
|
"policyAuthAddDefaultIdentityProvider": "Add Default Identity Provider",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "New role is required",
|
"accessRoleErrorNewRequired": "New role is required",
|
||||||
"accessRoleErrorRemove": "Failed to remove role",
|
"accessRoleErrorRemove": "Failed to remove role",
|
||||||
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
||||||
|
"accessRoleInferenceBudget": "AI Budget",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Configure how members of this role restrict AI usage based on spending or token limits",
|
||||||
"accessRoleName": "Role Name",
|
"accessRoleName": "Role Name",
|
||||||
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
|
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
|
||||||
"accessRoleRemove": "Remove Role",
|
"accessRoleRemove": "Remove Role",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
|
"actionCreateAiProvider": "Create AI Provider",
|
||||||
|
"actionDeleteAiProvider": "Delete AI Provider",
|
||||||
|
"actionGetAiProvider": "Get AI Provider",
|
||||||
|
"actionListAiProviders": "List AI Providers",
|
||||||
|
"actionUpdateAiProvider": "Update AI Provider",
|
||||||
|
"actionCreateAiModel": "Create AI Model",
|
||||||
|
"actionDeleteAiModel": "Delete AI Model",
|
||||||
|
"actionGetAiModel": "Get AI Model",
|
||||||
|
"actionListAiModels": "List AI Models",
|
||||||
|
"actionUpdateAiModel": "Update AI Model",
|
||||||
|
"actionCreateAiBudget": "Create AI Budget",
|
||||||
|
"actionDeleteAiBudget": "Delete AI Budget",
|
||||||
|
"actionGetAiBudget": "Get AI Budget",
|
||||||
|
"actionListAiBudgets": "List AI Budgets",
|
||||||
|
"actionUpdateAiBudget": "Update AI Budget",
|
||||||
|
"actionListResourceAiModels": "List Resource AI Models",
|
||||||
|
"actionSetResourceAiModels": "Set Resource AI Models",
|
||||||
|
"actionCreateVirtualApiKey": "Create Virtual API Key",
|
||||||
|
"actionDeleteVirtualApiKey": "Delete Virtual API Key",
|
||||||
|
"actionGetVirtualApiKey": "Get Virtual API Key",
|
||||||
|
"actionListVirtualApiKeys": "List Virtual API Keys",
|
||||||
|
"actionUpdateVirtualApiKey": "Update Virtual API Key",
|
||||||
"actionApplyBlueprint": "Apply Blueprint",
|
"actionApplyBlueprint": "Apply Blueprint",
|
||||||
"actionListBlueprints": "List Blueprints",
|
"actionListBlueprints": "List Blueprints",
|
||||||
"actionGetBlueprint": "Get Blueprint",
|
"actionGetBlueprint": "Get Blueprint",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Set Resource Pincode",
|
"actionSetResourcePincode": "Set Resource Pincode",
|
||||||
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
|
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
|
||||||
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
|
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
|
||||||
|
"actionListResourcePolicies": "List Resource Policies",
|
||||||
|
"actionCreateResourcePolicy": "Create Resource Policy",
|
||||||
"actionGetResourcePolicy": "Get Resource Policy",
|
"actionGetResourcePolicy": "Get Resource Policy",
|
||||||
"actionUpdateResourcePolicy": "Update Resource Policy",
|
"actionUpdateResourcePolicy": "Update Resource Policy",
|
||||||
|
"actionDeleteResourcePolicy": "Delete Resource Policy",
|
||||||
"actionSetResourcePolicyUsers": "Set Resource Policy Users",
|
"actionSetResourcePolicyUsers": "Set Resource Policy Users",
|
||||||
"actionSetResourcePolicyRoles": "Set Resource Policy Roles",
|
"actionSetResourcePolicyRoles": "Set Resource Policy Roles",
|
||||||
"actionSetResourcePolicyPassword": "Set Resource Policy Password",
|
"actionSetResourcePolicyPassword": "Set Resource Policy Password",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Invitations",
|
"sidebarInvitations": "Invitations",
|
||||||
"sidebarRoles": "Roles",
|
"sidebarRoles": "Roles",
|
||||||
"sidebarShareableLinks": "Shareable Links",
|
"sidebarShareableLinks": "Shareable Links",
|
||||||
|
"sidebarAiGateway": "AI Gateway",
|
||||||
|
"sidebarAiProviders": "Providers",
|
||||||
|
"commandAiProviders": "AI Providers",
|
||||||
|
"sidebarVirtualApiKeys": "Virtual API Keys",
|
||||||
|
"sidebarMyApiKeys": "Your API Keys",
|
||||||
|
"sidebarAccount": "Launcher",
|
||||||
|
"commandVirtualApiKeys": "Virtual API Keys",
|
||||||
|
"virtualApiKeysTitle": "Manage Virtual API Keys",
|
||||||
|
"virtualApiKeysDescription": "Create and manage manual API keys for AI Gateway access to public AI gateways",
|
||||||
|
"virtualApiKeysTabIdentity": "Identity Keys",
|
||||||
|
"virtualApiKeysTabVirtual": "Virtual Keys",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Identity Keys for Every User",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Every user already has a Pangolin identity key for this organization. It is unique to their account and authenticates them to AI gateways they can access.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Example",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "How Users Get Their Key",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Visit a public AI gateway URL in the browser and log in with their account.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Or go to <url></url> while signed in.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "You can create additional virtual API keys on the Virtual Keys tab. Those keys can be scoped to specific public AI gateways and optionally associated with a user. Creating a key immediately grants access to the selected public AI gateways.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Manually Create a Key",
|
||||||
|
"virtualApiKeysEmailIdentity": "Email Identity Keys",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Send each selected user or role their Pangolin identity key.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Send to all users",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Email every organization member who has an account email.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Users",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Roles",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Send Emails",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Identity keys emailed",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Sent {sent} emails.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} users were skipped because they do not have an email address.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Select at least one user or role, or send to all users.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Error sending identity keys",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Failed to email identity keys",
|
||||||
|
"virtualApiKeysBannerTitle": "Identity Keys for Every User",
|
||||||
|
"virtualApiKeysBannerDescription": "Every user already has an identity key available at {keysUrl}. You can also manually generate keys here that grant direct access to public AI gateways.",
|
||||||
|
"virtualApiKeysBannerButtonText": "View Identity Keys",
|
||||||
|
"virtualApiKeys": "Virtual API Keys",
|
||||||
|
"virtualApiKeysSearch": "Search keys...",
|
||||||
|
"virtualApiKeysCreate": "Create Virtual API Key",
|
||||||
|
"virtualApiKeysCreateDescription": "Mint a manual key that can call public AI gateways in this organization",
|
||||||
|
"virtualApiKeysCreateButton": "Create Key",
|
||||||
|
"virtualApiKeysEmpty": "No virtual API keys yet",
|
||||||
|
"virtualApiKeysName": "Name",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Description (optional)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Associate User (optional)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Associate this key with a user to track usage. Once created, this key immediately grants access to the selected public AI gateways. You do not need to associate a user to manually to the resource. The key will also show up in the user's profile.",
|
||||||
|
"virtualApiKeysAllResources": "All public AI gateways",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Allow this key to access every public AI gateway in the organization",
|
||||||
|
"virtualApiKeysSelectResources": "Public AI Gateways",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Select resources",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Choose which public AI gateways this key can access",
|
||||||
|
"virtualApiKeysNoResources": "No resources",
|
||||||
|
"virtualApiKeysSecret": "Key",
|
||||||
|
"virtualApiKeysCopyKey": "Copy this key. You can view it again later from the table or when editing.",
|
||||||
|
"virtualApiKeysViewSecret": "View Secret",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Virtual API Key Secret",
|
||||||
|
"virtualApiKeysViewSecretDescription": "This secret grants access to the public AI gateways assigned to this key",
|
||||||
|
"virtualApiKeysEdit": "Edit Virtual API Key",
|
||||||
|
"virtualApiKeysEditDescription": "Update the associated user and public AI gateway access for this key",
|
||||||
|
"virtualApiKeysSaveButton": "Save Changes",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Select at least one public AI gateway, or enable all public AI gateways",
|
||||||
|
"virtualApiKeysUpdated": "Virtual API key updated",
|
||||||
|
"virtualApiKeysUpdatedDescription": "The virtual API key has been updated",
|
||||||
|
"virtualApiKeysErrorUpdate": "Error updating virtual API key",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Failed to update virtual API key",
|
||||||
|
"virtualApiKeysErrorCreate": "Error creating virtual API key",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Failed to create virtual API key",
|
||||||
|
"virtualApiKeysErrorDelete": "Error deleting virtual API key",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Failed to delete virtual API key",
|
||||||
|
"virtualApiKeysDeleted": "Virtual API key deleted",
|
||||||
|
"virtualApiKeysDeletedDescription": "The virtual API key has been deleted",
|
||||||
|
"virtualApiKeysDelete": "Delete Virtual API Key",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Delete Key",
|
||||||
|
"virtualApiKeysQuestionRemove": "Are you sure you want to delete this virtual API key?",
|
||||||
|
"virtualApiKeysMessageRemove": "Clients using this key will lose access immediately.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Error loading secret",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Failed to load the virtual API key secret",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Unassigned",
|
||||||
|
"virtualApiKeysInferenceBudget": "Budget",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Configure how this key restricts AI usage based on spending or token limits",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Email key upon generation",
|
||||||
|
"virtualApiKeysEmailThisKey": "Email this key",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Send the key to the associated user and additional addresses after it is created",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Send the current key to the associated user and additional addresses",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "Email is not configured on this server",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Configure SMTP to email virtual API keys.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Send to associated user",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Email the key to the associated user's account email",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Associate a user to send the key to that user",
|
||||||
|
"virtualApiKeysEmailAdditional": "Additional emails",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Add email and press Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Select the associated user or add at least one email address",
|
||||||
|
"myVirtualApiKeysTitle": "Your API Keys",
|
||||||
|
"myVirtualApiKeysDescription": "View your identity key and any virtual API keys attributed to you in this organization",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Your API Keys for {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "View your identity key and virtual API keys attributed to you that can access {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Identity Key",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Your Personal API Key",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Your personal key for this organization. It is unique to your account and used to identify you when calling AI Gateway resources.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Your Personal API Key for {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Your personal key for this organization. Use it to call {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Attributed Keys",
|
||||||
|
"myVirtualApiKeysManualDescription": "Manual virtual API keys an admin associated with your account",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Manual virtual API keys associated with your account that can access {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "No attributed keys yet",
|
||||||
|
"myVirtualApiKeysKindUser": "Identity",
|
||||||
|
"myVirtualApiKeysKindManual": "Manual",
|
||||||
|
"myVirtualApiKeysUnnamed": "Unnamed key",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Reveal Secret",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "This secret authenticates you to AI Gateway resources",
|
||||||
|
"aiProvidersTitle": "AI Providers",
|
||||||
|
"aiProvidersDescription": "Connect model providers for AI workloads in this organization",
|
||||||
|
"aiProvidersBannerTitle": "Connect Model Providers",
|
||||||
|
"aiProvidersBannerDescription": "Providers are the model backends Pangolin uses for AI workloads. Connect OpenAI, Anthropic, and other providers here, then attach them to public AI gateways so users can call models with identity-aware access, budgets, and logging.",
|
||||||
|
"aiProvidersAdd": "Add Provider",
|
||||||
|
"aiProvidersSearch": "Search providers...",
|
||||||
|
"aiProvidersEmpty": "No AI providers yet",
|
||||||
|
"aiProviderCreate": "Create AI Provider",
|
||||||
|
"aiProviderCreateDescription": "Add a model provider for this organization",
|
||||||
|
"aiProviderSeeAll": "See All Providers",
|
||||||
|
"aiProviderSetting": "Provider Settings for {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Configure this AI provider",
|
||||||
|
"aiProviderGeneral": "General",
|
||||||
|
"aiProviderGeneralDescription": "Basic settings for this provider",
|
||||||
|
"aiProviderConfiguration": "Configuration",
|
||||||
|
"aiProviderConfigurationDescription": "Network routing and authentication for this provider",
|
||||||
|
"aiProviderNetworkSettings": "Network Settings",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Choose how traffic reaches this provider",
|
||||||
|
"aiProviderAuthSettings": "Authentication",
|
||||||
|
"aiProviderAuthSettingsDescription": "Configure how this provider authenticates requests to its upstream URL",
|
||||||
|
"aiProviderBudgetSettings": "Budget",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Configure how this provider restricts usage based on spending or token limits",
|
||||||
|
"aiBudgetAdd": "Add Budget",
|
||||||
|
"aiBudgetEmpty": "No budgets configured yet. Click Add Budget to set a spending or token limit.",
|
||||||
|
"aiBudgetUnit": "Spend Type",
|
||||||
|
"aiBudgetPeriod": "Reset Period",
|
||||||
|
"aiBudgetAmount": "Maximum Spend",
|
||||||
|
"aiBudgetAmountPlaceholder": "Maximum spend",
|
||||||
|
"aiBudgetPeriodHourly": "Hourly",
|
||||||
|
"aiBudgetPeriodDaily": "Daily",
|
||||||
|
"aiBudgetPeriodWeekly": "Weekly",
|
||||||
|
"aiBudgetPeriodMonthly": "Monthly",
|
||||||
|
"aiBudgetPeriodYearly": "Yearly",
|
||||||
|
"aiBudgetPeriodLifetime": "Lifetime",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Tokens",
|
||||||
|
"aiBudgetConflictError": "A budget for this reset period and spend type already exists",
|
||||||
|
"aiBudgetInvalidAmountError": "Enter a maximum spend greater than 0",
|
||||||
|
"aiBudgetUpdated": "Budgets updated",
|
||||||
|
"aiBudgetErrorSave": "Failed to update budgets",
|
||||||
|
"aiProviderType": "Provider Type",
|
||||||
|
"aiProviderTypeSearch": "Search providers...",
|
||||||
|
"aiProviderTypeNotFound": "No provider type found",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "Custom",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "Bring your own endpoint or route via site targets",
|
||||||
|
"aiProviderUpstreamUrl": "Upstream URL",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Base URL for the provider API",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Leave blank to use the default upstream URL for this provider",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Effective Upstream URL",
|
||||||
|
"aiProviderApiKey": "API Key",
|
||||||
|
"aiProviderApiKeyDescription": "API key used to authenticate requests to this provider",
|
||||||
|
"aiProviderCustomHeadersDescription": "Headers sent on every request to this provider. Newline separated: Header-Name: value",
|
||||||
|
"aiProviderApiKeyLastChars": "API Key",
|
||||||
|
"aiProviderAuthType": "Auth Type",
|
||||||
|
"aiProviderAuthTypeSearch": "Search auth types...",
|
||||||
|
"aiProviderAuthTypeNotFound": "No auth type found",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Authorization: Bearer key. Used by OpenAI and most providers",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key header. Used by Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key header. Used by Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Authorization: Splunk key. Used by Splunk HTTP Event Collector",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer key. Used by Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "No Auth",
|
||||||
|
"aiProviderAuthTypePassthrough": "Passthrough",
|
||||||
|
"aiProviderAuthTypeDescription": "How the upstream API authenticates requests",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Forward the caller's API key headers to the upstream",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Do not send authentication headers to the upstream",
|
||||||
|
"aiProviderRoutingMode": "Routing Mode",
|
||||||
|
"aiProviderRoutingModeDescription": "Send traffic to an upstream URL or to HTTP targets on your sites",
|
||||||
|
"aiProviderRoutingModeUrl": "Upstream URL",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Call a public or private API base URL",
|
||||||
|
"aiProviderRoutingModeTarget": "Site Targets",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Route through targets on your sites",
|
||||||
|
"aiProviderRoutingModeTargetNote": "After creating this provider, configure site targets on the Network Settings tab.",
|
||||||
|
"aiProviderTargetNoOne": "This provider doesn't have any targets. Add a target to route requests through your sites.",
|
||||||
|
"aiProviderSkipTlsVerification": "Skip TLS Verification",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Disable TLS certificate verification for the upstream connection",
|
||||||
|
"aiProviderBudget": "Budget",
|
||||||
|
"aiProviderBudgetDescription": "Optional spending or token budget for this provider",
|
||||||
|
"aiProviderBudgetAmount": "Budget Amount",
|
||||||
|
"aiProviderBudgetUnit": "Budget Unit",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Tokens",
|
||||||
|
"aiProviderEnabled": "Enabled",
|
||||||
|
"aiProviderEnabledDescription": "Fully disable this provider across all resources",
|
||||||
|
"aiProviderErrorCreate": "Failed to create AI provider",
|
||||||
|
"aiProviderErrorUpdate": "Failed to update AI provider",
|
||||||
|
"aiProviderErrorDelete": "Failed to delete AI provider",
|
||||||
|
"aiProviderErrorLoad": "Failed to load AI provider",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Enter a valid upstream URL",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Upstream URL is required for this provider",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Auth type is required",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API key is required",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Site targets routing is only available for custom providers",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Select at least one API capability",
|
||||||
|
"aiProviderCapabilities": "API Capabilities",
|
||||||
|
"aiProviderCapabilitiesDescription": "Select which API formats this provider can handle. Known providers start with recommended defaults.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Select which API formats this custom provider can handle",
|
||||||
|
"aiProviderCapabilitiesSelect": "Select capabilities",
|
||||||
|
"aiProviderCapabilitiesEmpty": "No capabilities found",
|
||||||
|
"aiProviderCapabilitiesSearch": "Search capabilities...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Chat Completions",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Supports /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI Responses",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Supports /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic Messages",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Supports /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini Generate Content",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Supports the direct Gemini API",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Bedrock Model Invoke",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Supports Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex Generate Content",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Supports Vertex AI Gemini format",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex Raw Predict",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Supports Vertex AI rawPredict for Anthropic models",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock Converse",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Supports Amazon Bedrock Converse API",
|
||||||
|
"aiProviderCreated": "AI provider created",
|
||||||
|
"aiProviderUpdated": "AI provider updated",
|
||||||
|
"aiProviderDeleted": "AI provider deleted",
|
||||||
|
"aiProviderDelete": "Delete Provider",
|
||||||
|
"aiProviderDeleteConfirm": "Delete Provider",
|
||||||
|
"aiProviderQuestionRemove": "Are you sure you want to delete this AI provider?",
|
||||||
|
"aiProviderMessageRemove": "This will permanently delete the provider and its models and targets. This cannot be undone.",
|
||||||
|
"aiProviderErrorNoUpdate": "AI provider is not available to update",
|
||||||
|
"aiProviderModels": "Models",
|
||||||
|
"aiProviderModelsDescription": "Define allow and block lists for this provider. Requests must match an allow entry and must not match a block entry.",
|
||||||
|
"aiProviderCreateModelsDescription": "Choose which models this provider can serve. An empty allow list denies all traffic. You can add or change models later.",
|
||||||
|
"aiProviderModelsPlaceholder": "Search models or type a custom key",
|
||||||
|
"aiProviderModelsAllow": "Allow List",
|
||||||
|
"aiProviderModelsAllowDescription": "Models that may be used through this provider. Empty means deny all.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Enter model key",
|
||||||
|
"aiProviderModelsAllowEmpty": "No models added yet.",
|
||||||
|
"aiProviderModelsBlock": "Block List",
|
||||||
|
"aiProviderModelsBlockDescription": "Models to deny even if they match an allow entry.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Enter model key",
|
||||||
|
"aiProviderModelsBlockEmpty": "No models added yet.",
|
||||||
|
"aiProviderModelsAdd": "Add Models",
|
||||||
|
"aiProviderModelsClearAll": "Clear All",
|
||||||
|
"aiProviderModelsAddCustom": "Add \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Press Enter to add this custom model key.",
|
||||||
|
"aiProviderModelsAddBulk": "Add {count} custom keys",
|
||||||
|
"aiProviderModelsAddBulkHint": "Press Enter to add {count} custom keys.",
|
||||||
|
"aiProviderModelsAddOne": "Add {key} now",
|
||||||
|
"aiProviderModelsAddSelected": "Add Selected",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} selected",
|
||||||
|
"aiProviderModelsSelectAll": "Select all",
|
||||||
|
"aiProviderModelsClearSelected": "Clear",
|
||||||
|
"aiProviderModelsBulkHint": "Select known models or type a custom key.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "No matching catalog models.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Known Models",
|
||||||
|
"aiProviderModelsAllLabel": "All models",
|
||||||
|
"aiProviderModelsAllPatternHint": "Wildcard: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Allow all models",
|
||||||
|
"aiProviderModelsAddAllBlock": "Block all models",
|
||||||
|
"aiProviderModelsAddAllDescription": "Uses the * wildcard so every model key matches.",
|
||||||
|
"aiProviderModelsViewMore": "View more ({count})",
|
||||||
|
"aiProviderModelsViewLess": "View less",
|
||||||
|
"aiProviderModelsRemove": "Remove model",
|
||||||
|
"aiProviderModelsEditHint": "Click to edit model settings",
|
||||||
|
"aiProviderModelsSourceCatalog": "Known catalog model",
|
||||||
|
"aiProviderModelsSourceCustom": "Custom model key",
|
||||||
|
"aiProviderModelsSourcePattern": "Wildcard pattern",
|
||||||
|
"aiProviderModelsSourceAll": "Matches every model key",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Budget configured",
|
||||||
|
"aiProviderModelsEditTitle": "Edit Model",
|
||||||
|
"aiProviderModelsEditDescription": "Update the model key or configure its budget.",
|
||||||
|
"aiProviderModelsBudgetTab": "Budget",
|
||||||
|
"aiProviderModelsKeyLabel": "Model Key",
|
||||||
|
"aiProviderModelsKeyRequired": "Enter a model key",
|
||||||
|
"aiProviderModelsKeyDuplicate": "This model key is already on a list",
|
||||||
|
"aiProviderModelsOverlapError": "These patterns cannot be on both lists: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Models updated",
|
||||||
|
"aiProviderModelsErrorUpdate": "Failed to update models",
|
||||||
|
"aiResourceProviders": "Providers",
|
||||||
|
"aiResourceProvidersDescription": "Choose which AI providers this AI gateway can use",
|
||||||
|
"aiResourceProvidersHelp": "Attach providers and choose inherit (use each provider's lists) or select (pick an allow list for this resource). Allow patterns that conflict across attached providers are not allowed.",
|
||||||
|
"aiResourceProvidersSelect": "Select providers",
|
||||||
|
"aiResourceProvidersEmpty": "No AI providers found",
|
||||||
|
"aiResourceProvidersNoneAttached": "No providers attached yet.",
|
||||||
|
"aiResourceProvidersAdd": "Add provider",
|
||||||
|
"aiResourceProvidersRemove": "Remove provider",
|
||||||
|
"aiResourceProviderToggleEnabled": "Enable or disable this provider on the resource",
|
||||||
|
"aiResourceProviderDisabled": "Disabled",
|
||||||
|
"aiResourceProvidersUpdated": "Providers updated",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Failed to update providers",
|
||||||
|
"aiResourceProviderEditDescription": "Choose how this provider's models are exposed on this resource.",
|
||||||
|
"viewProviderSettings": "View Provider Settings",
|
||||||
|
"aiResourceProviderMode": "Access mode",
|
||||||
|
"aiResourceProviderModeInherit": "Inherit",
|
||||||
|
"aiResourceProviderModeSelect": "Select",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Select · {count} models",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Use this provider's allow and block lists as configured on the provider.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Choose a subset of this provider's allow-list models for this resource.",
|
||||||
|
"aiResourceProviderAllowModels": "Allow list",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Select models",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Search models...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "No models found",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Only models from this provider's allow list can be selected.",
|
||||||
|
"aiResourceAliasRequired": "Alias is required for AI gateways",
|
||||||
|
"aiResourceDomainConfiguration": "Domain Configuration",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Choose the domain clients will use to reach this AI gateway.",
|
||||||
|
"aiUsageAnalyticsTitle": "AI Usage Analytics",
|
||||||
|
"aiUsageAnalyticsDescription": "Analyze AI gateway cost, token usage, and activity across providers, resources, roles, and users",
|
||||||
|
"aiUsageTabOverview": "Overview",
|
||||||
|
"aiUsageTabProviders": "Providers",
|
||||||
|
"aiUsageTabResources": "Resources",
|
||||||
|
"aiUsageFilterProvider": "Provider",
|
||||||
|
"aiUsageFilterModel": "Model",
|
||||||
|
"aiUsageFilterResource": "Resource",
|
||||||
|
"aiUsageFilterRole": "Role",
|
||||||
|
"aiUsageFilterUser": "User",
|
||||||
|
"aiUsageFilterAllProviders": "All Providers",
|
||||||
|
"aiUsageFilterAllModels": "All Models",
|
||||||
|
"aiUsageFilterAllResources": "All Resources",
|
||||||
|
"aiUsageFilterAllRoles": "All Roles",
|
||||||
|
"aiUsageFilterAllUsers": "All Users",
|
||||||
|
"aiUsageFilterSearch": "Search...",
|
||||||
|
"aiUsageFilterNotFound": "No options found",
|
||||||
|
"aiUsageResetFilters": "Reset Filters",
|
||||||
|
"aiUsageRefresh": "Refresh",
|
||||||
|
"aiUsageTokenTypePrompt": "Prompt",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Cache read",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Cache write",
|
||||||
|
"aiUsageTokenTypeCompletion": "Completion",
|
||||||
|
"aiUsageTokenTypeReasoning": "Reasoning",
|
||||||
|
"aiUsageRequests": "Requests",
|
||||||
|
"aiUsageCost": "Cost",
|
||||||
|
"aiUsageTokens": "Tokens",
|
||||||
|
"aiUsageOther": "Other",
|
||||||
|
"aiUsageTotalRequests": "Total Requests",
|
||||||
|
"aiUsageTotalTokens": "Total Tokens",
|
||||||
|
"aiUsageTotalCost": "Total Cost",
|
||||||
|
"aiUsageEstimated": "Estimated",
|
||||||
|
"aiUsageRequestVolume": "Request Volume",
|
||||||
|
"aiUsageTokenUsage": "Token Usage",
|
||||||
|
"aiUsageModelCost": "Model Cost",
|
||||||
|
"aiUsageModelTokens": "Model Tokens",
|
||||||
|
"aiUsageTopModels": "Top Models",
|
||||||
|
"aiUsageTopProviders": "Top Providers",
|
||||||
|
"aiUsageProviderCost": "Provider Cost",
|
||||||
|
"aiUsageProviderTokenUsage": "Provider Token Usage",
|
||||||
|
"aiUsageTopResources": "Top Resources",
|
||||||
|
"aiUsageResourceCost": "Resource Cost",
|
||||||
|
"aiUsageResourceTokenUsage": "Resource Token Usage",
|
||||||
|
"aiUsageResourceTypePublic": "Public Resource",
|
||||||
|
"aiUsageResourceTypeSite": "Private Resource",
|
||||||
|
"aiUsageNoResource": "No resource",
|
||||||
|
"aiUsageRolesTab": "Roles",
|
||||||
|
"aiUsageUsersTab": "Users",
|
||||||
|
"aiUsageTopRoles": "Top Roles",
|
||||||
|
"aiUsageRoleCost": "Role Cost",
|
||||||
|
"aiUsageRoleTokenUsage": "Role Token Usage",
|
||||||
|
"aiUsageTopUsers": "Top Users",
|
||||||
|
"aiUsageUserCost": "User Cost",
|
||||||
|
"aiUsageUserTokenUsage": "User Token Usage",
|
||||||
|
"aiUsageUnknownUser": "Unknown user",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Virtual API Keys",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Virtual API Key",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "All Virtual API Keys",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Top Virtual API Keys",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Virtual API Key Cost",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Virtual API Key Token Usage",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "No virtual API key",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Unnamed key",
|
||||||
|
"aiUsageLoading": "Loading...",
|
||||||
|
"aiUsageNoData": "No data",
|
||||||
|
"resourceBudgetSettings": "Budget",
|
||||||
|
"resourceBudgetSettingsDescription": "Configure how this AI gateway restricts usage based on spending or token limits",
|
||||||
"sidebarApiKeys": "API Keys",
|
"sidebarApiKeys": "API Keys",
|
||||||
"sidebarProvisioning": "Provisioning",
|
"sidebarProvisioning": "Provisioning",
|
||||||
"sidebarSettings": "Settings",
|
"sidebarSettings": "Settings",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Subnet",
|
"subnetPlaceholder": "Subnet",
|
||||||
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
||||||
"selectSites": "Select sites",
|
"selectSites": "Select sites",
|
||||||
|
"selectResources": "Select resources",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# resource} other {# resources}}",
|
||||||
"selectLabels": "Select labels",
|
"selectLabels": "Select labels",
|
||||||
"sitesDescription": "The client will have connectivity to the selected sites",
|
"sitesDescription": "The client will have connectivity to the selected sites",
|
||||||
"clientInstallOlm": "Install Machine Client",
|
"clientInstallOlm": "Install Machine Client",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Time is in seconds",
|
"timeIsInSeconds": "Time is in seconds",
|
||||||
"requireDeviceApproval": "Require Device Approvals",
|
"requireDeviceApproval": "Require Device Approvals",
|
||||||
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
|
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
|
||||||
"sshSettings": "SSH Settings",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI Gateway",
|
||||||
"sshAccess": "SSH Access",
|
"sshAccess": "SSH Access",
|
||||||
"rdpSettings": "RDP Settings",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "VNC Settings",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH Server",
|
"sshServer": "SSH Server",
|
||||||
"rdpServer": "RDP Server",
|
"rdpServer": "RDP Server",
|
||||||
"vncServer": "VNC Server",
|
"vncServer": "VNC Server",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI Gateway",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Scheme",
|
"editInternalResourceDialogScheme": "Scheme",
|
||||||
"editInternalResourceDialogEnableSsl": "Enable TLS",
|
"editInternalResourceDialogEnableSsl": "Enable TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Allow ICMP Ping",
|
"privateResourceAllowIcmpPing": "Allow ICMP Ping",
|
||||||
"privateResourceNetworkAccess": "Network Access",
|
"privateResourceNetworkAccess": "Network Access",
|
||||||
"privateResourceNetworkAccessDescription": "Control TCP/UDP port access and whether ICMP ping is allowed for this resource.",
|
"privateResourceNetworkAccessDescription": "Control TCP/UDP port access and whether ICMP ping is allowed for this resource.",
|
||||||
"hostSettings": "Host Settings",
|
"hostSettings": "Host",
|
||||||
"cidrSettings": "CIDR Settings",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
||||||
"createInternalResourceDialogName": "Name",
|
"createInternalResourceDialogName": "Name",
|
||||||
"createInternalResourceDialogSite": "Site",
|
"createInternalResourceDialogSite": "Site",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI Gateway",
|
||||||
"scheme": "Scheme",
|
"scheme": "Scheme",
|
||||||
"createInternalResourceDialogScheme": "Scheme",
|
"createInternalResourceDialogScheme": "Scheme",
|
||||||
"createInternalResourceDialogEnableSsl": "Enable TLS",
|
"createInternalResourceDialogEnableSsl": "Enable TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Valid Password",
|
"validPassword": "Valid Password",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Valid Virtual API Key",
|
||||||
"view": "View",
|
"view": "View",
|
||||||
"configManaged": "Config Managed",
|
"configManaged": "Config Managed",
|
||||||
"connectedClient": "Connected Client",
|
"connectedClient": "Connected Client",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Log Retention",
|
"logRetention": "Log Retention",
|
||||||
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
||||||
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
|
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
|
||||||
|
"aiSessionLogs": "AI Gateway Session Logs",
|
||||||
|
"aiSessionLogsDescription": "View prompt and response transcripts for AI gateway requests in this organization",
|
||||||
|
"sidebarLogsAi": "Session Logs",
|
||||||
|
"commandLogsAi": "Session Logs",
|
||||||
|
"sidebarLogsAiUsage": "Usage Analytics",
|
||||||
|
"commandLogsAiUsage": "Usage Analytics",
|
||||||
|
"provider": "Provider",
|
||||||
|
"capability": "Capability",
|
||||||
|
"model": "Model",
|
||||||
|
"virtualApiKey": "Virtual API Key",
|
||||||
|
"noVirtualApiKey": "No virtual API key",
|
||||||
|
"stream": "Stream",
|
||||||
|
"streaming": "Streaming",
|
||||||
|
"nonStreaming": "Non-streaming",
|
||||||
|
"statusCode": "Status Code",
|
||||||
|
"aiSessionId": "Session ID",
|
||||||
|
"aiSessionRequest": "Request",
|
||||||
|
"aiSessionResponse": "Response",
|
||||||
|
"aiSessionNoData": "No data captured",
|
||||||
|
"aiSessionCouldNotParse": "(raw, could not parse transcript)",
|
||||||
|
"aiSessionLogTruncated": "This session was truncated before storage and may be incomplete.",
|
||||||
|
"aiSessionViewRaw": "View Raw JSON",
|
||||||
|
"aiSessionViewChat": "View Chat",
|
||||||
|
"cost": "Cost",
|
||||||
|
"estimated": "Estimated",
|
||||||
|
"tokenUsage": "Token Usage",
|
||||||
|
"promptTokens": "Prompt Tokens",
|
||||||
|
"cacheReadTokens": "Cache Read Tokens",
|
||||||
|
"cacheWriteTokens": "Cache Write Tokens",
|
||||||
|
"completionTokens": "Completion Tokens",
|
||||||
|
"reasoningTokens": "Reasoning Tokens",
|
||||||
|
"totalTokens": "Total Tokens",
|
||||||
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
|
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
|
||||||
"logRetentionRequestLabel": "HTTP Request Log Retention",
|
"logRetentionRequestLabel": "HTTP Request Log Retention",
|
||||||
"logRetentionRequestDescription": "How long to retain request logs",
|
"logRetentionRequestDescription": "How long to retain request logs",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "How long to retain action logs",
|
"logRetentionActionDescription": "How long to retain action logs",
|
||||||
"logRetentionConnectionLabel": "Network Log Retention",
|
"logRetentionConnectionLabel": "Network Log Retention",
|
||||||
"logRetentionConnectionDescription": "How long to retain connection logs",
|
"logRetentionConnectionDescription": "How long to retain connection logs",
|
||||||
|
"logRetentionAISessionsLabel": "AI Gateway Session Log Retention",
|
||||||
|
"logRetentionAISessionsDescription": "How long to retain AI gateway prompt/response session logs",
|
||||||
"logRetentionDisabled": "Disabled",
|
"logRetentionDisabled": "Disabled",
|
||||||
"logRetention3Days": "3 days",
|
"logRetention3Days": "3 days",
|
||||||
"logRetention7Days": "7 days",
|
"logRetention7Days": "7 days",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Source Address",
|
"sourceAddress": "Source Address",
|
||||||
"destinationAddress": "Destination Address",
|
"destinationAddress": "Destination Address",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or trial</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or trial</bookADemoLink>",
|
||||||
"certResolver": "Certificate Resolver",
|
"certResolver": "Certificate Resolver",
|
||||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||||
"selectCertResolver": "Select Certificate Resolver",
|
"selectCertResolver": "Select Certificate Resolver",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Selecting multiple sites enables resilient routing and failover for high availability.",
|
"internalResourceFormMultiSiteRoutingHelp": "Selecting multiple sites enables resilient routing and failover for high availability.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Learn more",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Learn more",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "HTTP configuration",
|
"createInternalResourceDialogHttpConfiguration": "Domain Configuration",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "HTTP configuration",
|
"editInternalResourceDialogHttpConfiguration": "Domain Configuration",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Unlabeled",
|
"resourceLauncherUnlabeled": "Unlabeled",
|
||||||
|
"resourceLauncherAiGateway": "AI Gateway",
|
||||||
"resourceLauncherNoSite": "No Site",
|
"resourceLauncherNoSite": "No Site",
|
||||||
|
"resourceLauncherAvailableModels": "Available Models",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Models you can use with this AI gateway.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "No models are available for this resource.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Could not load available models.",
|
||||||
|
"resourceLauncherApiKeys": "API Keys",
|
||||||
|
"resourceLauncherApiKeysDescription": "Use your identity key or an attributed key to authenticate with this resource.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Identity Key",
|
||||||
|
"resourceLauncherApiKeysManual": "Attributed Keys",
|
||||||
|
"resourceLauncherApiKeysEmpty": "No API keys are available for this resource.",
|
||||||
|
"resourceLauncherApiKeysError": "Could not load API keys.",
|
||||||
"resourceLauncherNoResourcesInGroup": "No resources in this group",
|
"resourceLauncherNoResourcesInGroup": "No resources in this group",
|
||||||
"resourceLauncherEmptyStateTitle": "No Resources Available",
|
"resourceLauncherEmptyStateTitle": "No Resources Available",
|
||||||
"resourceLauncherEmptyStateDescription": "You don't have access to any resources yet. Contact your administrator to request access.",
|
"resourceLauncherEmptyStateDescription": "You don't have access to any resources yet. Contact your administrator to request access.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Could not delete the launcher view. Please try again.",
|
"resourceLauncherViewDeleteFailedDescription": "Could not delete the launcher view. Please try again.",
|
||||||
"memberPortalPrevious": "Previous",
|
"memberPortalPrevious": "Previous",
|
||||||
"memberPortalNext": "Next",
|
"memberPortalNext": "Next",
|
||||||
"httpSettings": "HTTP Settings",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "TCP Settings",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "UDP Settings",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Establishing a secure connection…",
|
"sshConnectingDescription": "Establishing a secure connection…",
|
||||||
"sshConnecting": "Connecting…",
|
"sshConnecting": "Connecting…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Por favor, asegúrese de que ha iniciado sesión como el usuario correcto.",
|
"inviteLoginUser": "Por favor, asegúrese de que ha iniciado sesión como el usuario correcto.",
|
||||||
"inviteErrorNoUser": "Lo sentimos, pero parece que la invitación a la que intentas acceder no es para un usuario que existe.",
|
"inviteErrorNoUser": "Lo sentimos, pero parece que la invitación a la que intentas acceder no es para un usuario que existe.",
|
||||||
"inviteCreateUser": "Por favor, cree una cuenta primero.",
|
"inviteCreateUser": "Por favor, cree una cuenta primero.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Las invitaciones solo pueden ser aceptadas por cuentas internas. Cierra sesión e inicia sesión con tu contraseña para este correo electrónico.",
|
||||||
|
"inviteLoginInternalOnly": "Las invitaciones requieren una cuenta interna con una contraseña. Crea una cuenta o inicia sesión con tu contraseña.",
|
||||||
"goHome": "Ir a casa",
|
"goHome": "Ir a casa",
|
||||||
"inviteLogInOtherUser": "Iniciar sesión como un usuario diferente",
|
"inviteLogInOtherUser": "Iniciar sesión como un usuario diferente",
|
||||||
"createAnAccount": "Crear una cuenta",
|
"createAnAccount": "Crear una cuenta",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Ningún país encontrado.",
|
"noCountryFound": "Ningún país encontrado.",
|
||||||
"siteSelectionDescription": "Este sitio proporcionará conectividad al objetivo.",
|
"siteSelectionDescription": "Este sitio proporcionará conectividad al objetivo.",
|
||||||
"resourceType": "Tipo de recurso",
|
"resourceType": "Tipo de recurso",
|
||||||
"resourceTypeDescription": "Esto controla el protocolo del recurso y cómo se renderizará en el navegador. Esto no se puede cambiar más tarde.",
|
"resourceTypeDescription": "Esto controla el protocolo del recurso y no se puede cambiar más tarde.",
|
||||||
|
"resourceTypeSearch": "Buscar tipos de recursos...",
|
||||||
|
"resourceTypeNotFound": "No se encontró ningún tipo de recurso",
|
||||||
|
"resourceTypeHttpDescription": "Proxy de tráfico web a través de HTTPS usando un nombre de dominio",
|
||||||
|
"resourceTypeInferenceDescription": "Dirige las solicitudes de IA a través de proveedores adjuntos",
|
||||||
|
"resourceTypeSshDescription": "Accede a un servidor SSH desde el navegador",
|
||||||
|
"resourceTypeRdpDescription": "Accede a un escritorio remoto desde el navegador",
|
||||||
|
"resourceTypeVncDescription": "Accede a un escritorio VNC desde el navegador",
|
||||||
|
"resourceTypeTcpDescription": "Proxy de tráfico TCP crudo usando un número de puerto",
|
||||||
|
"resourceTypeUdpDescription": "Proxy de tráfico UDP crudo usando un número de puerto",
|
||||||
|
"privateResourceTypeDescription": "Esto controla cómo los clientes acceden al recurso. No se puede cambiar más tarde.",
|
||||||
|
"privateResourceTypeHostDescription": "Expone un host único en la red del sitio a los clientes conectados",
|
||||||
|
"privateResourceTypeCidrDescription": "Expón un rango CIDR en la red del sitio para los clientes conectados",
|
||||||
|
"privateResourceTypeHttpDescription": "Accede a un servicio HTTP o HTTPS a través de un dominio",
|
||||||
|
"privateResourceTypeSshDescription": "Accede a un servidor SSH desde clientes conectados",
|
||||||
"resourceDomainDescription": "El recurso se servirá en este nombre de dominio completamente calificado.",
|
"resourceDomainDescription": "El recurso se servirá en este nombre de dominio completamente calificado.",
|
||||||
"resourceHTTPSSettings": "Configuración HTTPS",
|
"resourceHTTPSSettings": "Configuración HTTPS",
|
||||||
"resourceHTTPSSettingsDescription": "Configurar cómo se accederá al recurso a través de HTTPS",
|
"resourceHTTPSSettingsDescription": "Configurar cómo se accederá al recurso a través de HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Guardar ajustes",
|
"authMethodsSave": "Guardar ajustes",
|
||||||
"policyAuthStackTitle": "Autenticación",
|
"policyAuthStackTitle": "Autenticación",
|
||||||
"policyAuthStackDescription": "Controla qué métodos de autenticación son necesarios para acceder a este recurso",
|
"policyAuthStackDescription": "Controla qué métodos de autenticación son necesarios para acceder a este recurso",
|
||||||
|
"policyAuthInferenceStackDescription": "Elige qué usuarios y roles pueden autenticarse en este gateway de IA",
|
||||||
"policyAuthOrLogicTitle": "Múltiples métodos de autenticación activos",
|
"policyAuthOrLogicTitle": "Múltiples métodos de autenticación activos",
|
||||||
"policyAuthOrLogicBanner": "Los visitantes pueden autenticarse utilizando cualquier método activo a continuación. No necesitan completar todos ellos.",
|
"policyAuthOrLogicBanner": "Los visitantes pueden autenticarse utilizando cualquier método activo a continuación. No necesitan completar todos ellos.",
|
||||||
"policyAuthMethodActive": "Activo",
|
"policyAuthMethodActive": "Activo",
|
||||||
"policyAuthMethodOff": "Apagado",
|
"policyAuthMethodOff": "Apagado",
|
||||||
"policyAuthSsoTitle": "SSO de Plataforma",
|
"policyAuthSsoTitle": "SSO de Plataforma",
|
||||||
"policyAuthSsoDescription": "Requiere iniciar sesión a través del proveedor de identidad de tu organización",
|
"policyAuthSsoDescription": "Requiere iniciar sesión a través del proveedor de identidad de tu organización",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "URL de inicio de sesión",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Cada usuario ya tiene una clave API de identidad, por lo que solo necesitas crear claves API virtuales para clientes no usuarios o acceso compartido. Los usuarios pueden recuperar su clave iniciando sesión con su proveedor de identidad en la URL de este recurso, donde se mostrará después del inicio de sesión.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Cada usuario ya tiene una clave API de identidad, por lo que solo necesitas crear claves API virtuales para clientes no usuarios o acceso compartido. Los usuarios recuperan su clave aquí después de iniciar sesión con su proveedor de identidad.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} usuarios, {roles} roles",
|
"policyAuthSsoSummary": "{idp} · {users} usuarios, {roles} roles",
|
||||||
"policyAuthSsoDefaultIdp": "Proveedor por defecto",
|
"policyAuthSsoDefaultIdp": "Proveedor por defecto",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Añadir Proveedor de Identidad Predeterminado",
|
"policyAuthAddDefaultIdentityProvider": "Añadir Proveedor de Identidad Predeterminado",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Se requiere un nuevo rol",
|
"accessRoleErrorNewRequired": "Se requiere un nuevo rol",
|
||||||
"accessRoleErrorRemove": "Error al eliminar el rol",
|
"accessRoleErrorRemove": "Error al eliminar el rol",
|
||||||
"accessRoleErrorRemoveDescription": "Ocurrió un error mientras se eliminaba el rol.",
|
"accessRoleErrorRemoveDescription": "Ocurrió un error mientras se eliminaba el rol.",
|
||||||
|
"accessRoleInferenceBudget": "Presupuesto de IA",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Configura cómo los miembros de este rol restringen el uso de IA según los límites de gasto o de tokens",
|
||||||
"accessRoleName": "Nombre del Rol",
|
"accessRoleName": "Nombre del Rol",
|
||||||
"accessRoleQuestionRemove": "Estás a punto de eliminar el rol `{name}`. No puedes deshacer esta acción.",
|
"accessRoleQuestionRemove": "Estás a punto de eliminar el rol `{name}`. No puedes deshacer esta acción.",
|
||||||
"accessRoleRemove": "Quitar rol",
|
"accessRoleRemove": "Quitar rol",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Eliminar sitio",
|
"actionDeleteSite": "Eliminar sitio",
|
||||||
"actionGetSite": "Obtener sitio",
|
"actionGetSite": "Obtener sitio",
|
||||||
"actionListSites": "Listar sitios",
|
"actionListSites": "Listar sitios",
|
||||||
|
"actionCreateAiProvider": "Crear Proveedor de IA",
|
||||||
|
"actionDeleteAiProvider": "Eliminar Proveedor de IA",
|
||||||
|
"actionGetAiProvider": "Obtener Proveedor de IA",
|
||||||
|
"actionListAiProviders": "Lista de Proveedores de IA",
|
||||||
|
"actionUpdateAiProvider": "Actualizar Proveedor de IA",
|
||||||
|
"actionCreateAiModel": "Crear Modelo de IA",
|
||||||
|
"actionDeleteAiModel": "Eliminar Modelo de IA",
|
||||||
|
"actionGetAiModel": "Obtener Modelo de IA",
|
||||||
|
"actionListAiModels": "Lista de Modelos de IA",
|
||||||
|
"actionUpdateAiModel": "Actualizar Modelo de IA",
|
||||||
|
"actionCreateAiBudget": "Crear Presupuesto de IA",
|
||||||
|
"actionDeleteAiBudget": "Eliminar Presupuesto de IA",
|
||||||
|
"actionGetAiBudget": "Obtener Presupuesto de IA",
|
||||||
|
"actionListAiBudgets": "Lista de Presupuestos de IA",
|
||||||
|
"actionUpdateAiBudget": "Actualizar Presupuesto de IA",
|
||||||
|
"actionListResourceAiModels": "Lista de Modelos de IA de Recursos",
|
||||||
|
"actionSetResourceAiModels": "Establecer Modelos de IA de Recursos",
|
||||||
|
"actionCreateVirtualApiKey": "Crear Clave API Virtual",
|
||||||
|
"actionDeleteVirtualApiKey": "Eliminar Clave API Virtual",
|
||||||
|
"actionGetVirtualApiKey": "Obtener Clave API Virtual",
|
||||||
|
"actionListVirtualApiKeys": "Lista de Claves API Virtuales",
|
||||||
|
"actionUpdateVirtualApiKey": "Actualizar Clave API Virtual",
|
||||||
"actionApplyBlueprint": "Aplicar plano",
|
"actionApplyBlueprint": "Aplicar plano",
|
||||||
"actionListBlueprints": "Listar blueprints",
|
"actionListBlueprints": "Listar blueprints",
|
||||||
"actionGetBlueprint": "Obtener blueprint",
|
"actionGetBlueprint": "Obtener blueprint",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Establecer Pincode del recurso",
|
"actionSetResourcePincode": "Establecer Pincode del recurso",
|
||||||
"actionSetResourceEmailWhitelist": "Establecer lista blanca de correo de recursos",
|
"actionSetResourceEmailWhitelist": "Establecer lista blanca de correo de recursos",
|
||||||
"actionGetResourceEmailWhitelist": "Obtener correo electrónico de recursos",
|
"actionGetResourceEmailWhitelist": "Obtener correo electrónico de recursos",
|
||||||
|
"actionListResourcePolicies": "Lista de Políticas de Recursos",
|
||||||
|
"actionCreateResourcePolicy": "Crear Política de Recursos",
|
||||||
"actionGetResourcePolicy": "Obtener política de recursos",
|
"actionGetResourcePolicy": "Obtener política de recursos",
|
||||||
"actionUpdateResourcePolicy": "Actualizar política de recursos",
|
"actionUpdateResourcePolicy": "Actualizar política de recursos",
|
||||||
|
"actionDeleteResourcePolicy": "Eliminar Política de Recursos",
|
||||||
"actionSetResourcePolicyUsers": "Definir política de recursos para usuarios",
|
"actionSetResourcePolicyUsers": "Definir política de recursos para usuarios",
|
||||||
"actionSetResourcePolicyRoles": "Definir roles de política de recursos",
|
"actionSetResourcePolicyRoles": "Definir roles de política de recursos",
|
||||||
"actionSetResourcePolicyPassword": "Definir contraseña de política de recursos",
|
"actionSetResourcePolicyPassword": "Definir contraseña de política de recursos",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Invitaciones",
|
"sidebarInvitations": "Invitaciones",
|
||||||
"sidebarRoles": "Roles",
|
"sidebarRoles": "Roles",
|
||||||
"sidebarShareableLinks": "Enlaces Compartibles",
|
"sidebarShareableLinks": "Enlaces Compartibles",
|
||||||
|
"sidebarAiGateway": "Gateway de IA",
|
||||||
|
"sidebarAiProviders": "Proveedores",
|
||||||
|
"commandAiProviders": "Proveedores de IA",
|
||||||
|
"sidebarVirtualApiKeys": "Claves API Virtuales",
|
||||||
|
"sidebarMyApiKeys": "Tus Claves API",
|
||||||
|
"sidebarAccount": "Lanzador",
|
||||||
|
"commandVirtualApiKeys": "Claves API Virtuales",
|
||||||
|
"virtualApiKeysTitle": "Administrar Claves API Virtuales",
|
||||||
|
"virtualApiKeysDescription": "Crea y gestiona claves API manuales para el acceso al Gateway público de IA",
|
||||||
|
"virtualApiKeysTabIdentity": "Claves de Identidad",
|
||||||
|
"virtualApiKeysTabVirtual": "Claves Virtuales",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Claves de Identidad para Cada Usuario",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Cada usuario ya tiene una clave de identidad Pangolin para esta organización. Es única para su cuenta y los autentica en gateways de IA a los que pueden acceder.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Ejemplo",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Cómo los Usuarios Obtienen su Clave",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Visita una URL de gateway público de IA en el navegador e inicia sesión con su cuenta.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "O ve a <url></url> mientras estás conectado.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Puedes crear claves API virtuales adicionales en la pestaña de Claves Virtuales. Esas claves pueden estar limitadas a gateways públicos de IA específicos y opcionalmente asociadas a un usuario. Crear una clave concede inmediatamente acceso a los gateways públicos de IA seleccionados.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Crear Manualmente una Clave",
|
||||||
|
"virtualApiKeysEmailIdentity": "Claves de Identidad por Correo Electrónico",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Envía a cada usuario o rol seleccionado su clave de identidad Pangolin.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Enviar a todos los usuarios",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Enviar correo electrónico a cada miembro de la organización que tenga un correo electrónico de cuenta.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Usuarios",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Roles",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Enviar Correos Electrónicos",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Claves de identidad enviadas por correo electrónico",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Se enviaron {sent} correos electrónicos.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} usuarios se omitieron porque no tienen una dirección de correo electrónico.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Selecciona al menos un usuario o rol, o envía a todos los usuarios.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Error al enviar claves de identidad",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "No se pudieron enviar por correo electrónico las claves de identidad",
|
||||||
|
"virtualApiKeysBannerTitle": "Claves de Identidad para Cada Usuario",
|
||||||
|
"virtualApiKeysBannerDescription": "Cada usuario ya tiene una clave de identidad disponible en {keysUrl}. También puedes generar manualmente claves aquí que conceden acceso directo a gateways públicos de IA.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Ver Claves de Identidad",
|
||||||
|
"virtualApiKeys": "Claves API Virtuales",
|
||||||
|
"virtualApiKeysSearch": "Buscar claves...",
|
||||||
|
"virtualApiKeysCreate": "Crear Clave API Virtual",
|
||||||
|
"virtualApiKeysCreateDescription": "Genera una clave manual que pueda llamar a gateways públicos de IA en esta organización",
|
||||||
|
"virtualApiKeysCreateButton": "Crear Clave",
|
||||||
|
"virtualApiKeysEmpty": "Aún no hay claves API virtuales",
|
||||||
|
"virtualApiKeysName": "Nombre",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Descripción (opcional)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Asociar Usuario (opcional)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Asocia esta clave con un usuario para rastrear el uso. Una vez creada, esta clave concede inmediatamente acceso a los gateways públicos de IA seleccionados. No necesitas asociar un usuario manualmente al recurso. La clave también aparecerá en el perfil del usuario.",
|
||||||
|
"virtualApiKeysAllResources": "Todos los gateways públicos de IA",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Permitir que esta clave acceda a cada gateway público de IA en la organización",
|
||||||
|
"virtualApiKeysSelectResources": "Gateways Públicos de IA",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Seleccionar recursos",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Elige a qué gateways públicos de IA puede acceder esta clave",
|
||||||
|
"virtualApiKeysNoResources": "Sin recursos",
|
||||||
|
"virtualApiKeysSecret": "Clave",
|
||||||
|
"virtualApiKeysCopyKey": "Copia esta clave. Puedes verla nuevamente más tarde desde la tabla o al editar.",
|
||||||
|
"virtualApiKeysViewSecret": "Ver Secreto",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Secreto de la Clave API Virtual",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Este secreto concede acceso a los gateways públicos de IA asignados a esta clave",
|
||||||
|
"virtualApiKeysEdit": "Editar Clave API Virtual",
|
||||||
|
"virtualApiKeysEditDescription": "Actualiza el usuario asociado y el acceso al gateway público de IA para esta clave",
|
||||||
|
"virtualApiKeysSaveButton": "Guardar Cambios",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Selecciona al menos un gateway público de IA, o habilita todos los gateways públicos de IA",
|
||||||
|
"virtualApiKeysUpdated": "Clave API virtual actualizada",
|
||||||
|
"virtualApiKeysUpdatedDescription": "La clave API virtual ha sido actualizada",
|
||||||
|
"virtualApiKeysErrorUpdate": "Error al actualizar la clave API virtual",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Error al actualizar clave API virtual",
|
||||||
|
"virtualApiKeysErrorCreate": "Error al crear clave API virtual",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Error al crear clave API virtual",
|
||||||
|
"virtualApiKeysErrorDelete": "Error al eliminar clave API virtual",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Error al eliminar clave API virtual",
|
||||||
|
"virtualApiKeysDeleted": "Clave API virtual eliminada",
|
||||||
|
"virtualApiKeysDeletedDescription": "La clave API virtual ha sido eliminada",
|
||||||
|
"virtualApiKeysDelete": "Eliminar Clave API Virtual",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Eliminar Clave",
|
||||||
|
"virtualApiKeysQuestionRemove": "¿Está seguro de que desea eliminar esta clave API virtual?",
|
||||||
|
"virtualApiKeysMessageRemove": "Los clientes que usen esta clave perderán acceso inmediatamente.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Error al cargar el secreto",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Error al cargar el secreto de la clave API virtual",
|
||||||
|
"virtualApiKeysFilterUnassigned": "No asignado",
|
||||||
|
"virtualApiKeysInferenceBudget": "Presupuesto",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Configura cómo esta clave restringe el uso de IA según los límites de gasto o de tokens",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Correo electrónico al generar",
|
||||||
|
"virtualApiKeysEmailThisKey": "Envía este correo electrónico",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Envía la clave al usuario asociado y direcciones adicionales después de que se cree",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Envía la clave actual al usuario asociado y direcciones adicionales",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "El correo electrónico no está configurado en este servidor",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Configure SMTP para enviar claves API virtuales por correo electrónico.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Enviar al usuario asociado",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Envía la clave al correo electrónico de la cuenta del usuario asociado",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Asocia un usuario para enviar la clave a ese usuario",
|
||||||
|
"virtualApiKeysEmailAdditional": "Correos electrónicos adicionales",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Agrega un correo electrónico y presiona Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Selecciona el usuario asociado o agrega al menos una dirección de correo electrónico",
|
||||||
|
"myVirtualApiKeysTitle": "Tus Claves API",
|
||||||
|
"myVirtualApiKeysDescription": "Ve tu clave de identidad y cualquier clave API virtuales atribuidas a ti en esta organización",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Tus Claves API para {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Ve tu clave de identidad y claves API virtuales atribuidas a ti que pueden acceder a {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Clave de Identidad",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Tu Clave API Personal",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Tu clave personal para esta organización. Es única para tu cuenta y se utiliza para identificarte cuando llamas a recursos del Gateway de IA.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Tu Clave API Personal para {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Tu clave personal para esta organización. Úsala para llamar a {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Claves Atribuidas",
|
||||||
|
"myVirtualApiKeysManualDescription": "Claves API virtuales manuales que un administrador ha asociado con tu cuenta",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Claves API virtuales manuales asociadas a tu cuenta que pueden acceder a {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Aún no hay claves atribuidas",
|
||||||
|
"myVirtualApiKeysKindUser": "Identidad",
|
||||||
|
"myVirtualApiKeysKindManual": "Manual",
|
||||||
|
"myVirtualApiKeysUnnamed": "Clave sin nombre",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Revelar Secreto",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Este secreto te autentica en los recursos del Gateway de IA",
|
||||||
|
"aiProvidersTitle": "Proveedores de IA",
|
||||||
|
"aiProvidersDescription": "Conecta proveedores de modelos para cargas de trabajo de IA en esta organización",
|
||||||
|
"aiProvidersBannerTitle": "Conecta Proveedores de Modelos",
|
||||||
|
"aiProvidersBannerDescription": "Los proveedores son los backends de modelo que Pangolin usa para cargas de trabajo de IA. Conecta OpenAI, Anthropic y otros proveedores aquí, luego adjúntalos a gateways públicos de IA para que los usuarios puedan llamar a modelos con acceso consciente de identidad, presupuestos y registros.",
|
||||||
|
"aiProvidersAdd": "Agregar Proveedor",
|
||||||
|
"aiProvidersSearch": "Buscar proveedores...",
|
||||||
|
"aiProvidersEmpty": "Aún no hay proveedores de IA",
|
||||||
|
"aiProviderCreate": "Crear Proveedor de IA",
|
||||||
|
"aiProviderCreateDescription": "Agrega un proveedor de modelos para esta organización",
|
||||||
|
"aiProviderSeeAll": "Ver Todos los Proveedores",
|
||||||
|
"aiProviderSetting": "Ajustes del Proveedor para {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Configura este proveedor de IA",
|
||||||
|
"aiProviderGeneral": "General",
|
||||||
|
"aiProviderGeneralDescription": "Configuraciones básicas para este proveedor",
|
||||||
|
"aiProviderConfiguration": "Configuración",
|
||||||
|
"aiProviderConfigurationDescription": "Enrutamiento de red y autenticación para este proveedor",
|
||||||
|
"aiProviderNetworkSettings": "Configuraciones de Red",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Elige cómo llega el tráfico a este proveedor",
|
||||||
|
"aiProviderAuthSettings": "Autenticación",
|
||||||
|
"aiProviderAuthSettingsDescription": "Configura cómo este proveedor autentica solicitudes a su URL upstream",
|
||||||
|
"aiProviderBudgetSettings": "Presupuesto",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Configura cómo este proveedor restringe el uso basado en los límites de gasto o de tokens",
|
||||||
|
"aiBudgetAdd": "Agregar Presupuesto",
|
||||||
|
"aiBudgetEmpty": "Aún no hay presupuestos configurados. Haz clic en Agregar Presupuesto para establecer un límite de gasto o de tokens.",
|
||||||
|
"aiBudgetUnit": "Tipo de Gasto",
|
||||||
|
"aiBudgetPeriod": "Período de Restablecimiento",
|
||||||
|
"aiBudgetAmount": "Gasto Máximo",
|
||||||
|
"aiBudgetAmountPlaceholder": "Gasto máximo",
|
||||||
|
"aiBudgetPeriodHourly": "Por hora",
|
||||||
|
"aiBudgetPeriodDaily": "Diario",
|
||||||
|
"aiBudgetPeriodWeekly": "Semanal",
|
||||||
|
"aiBudgetPeriodMonthly": "Mensual",
|
||||||
|
"aiBudgetPeriodYearly": "Anual",
|
||||||
|
"aiBudgetPeriodLifetime": "De por vida",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Tokens",
|
||||||
|
"aiBudgetConflictError": "Ya existe un presupuesto para este período de restablecimiento y tipo de gasto",
|
||||||
|
"aiBudgetInvalidAmountError": "Ingresa un gasto máximo mayor a 0",
|
||||||
|
"aiBudgetUpdated": "Presupuestos actualizados",
|
||||||
|
"aiBudgetErrorSave": "Error al actualizar presupuestos",
|
||||||
|
"aiProviderType": "Tipo de Proveedor",
|
||||||
|
"aiProviderTypeSearch": "Buscar proveedores...",
|
||||||
|
"aiProviderTypeNotFound": "No se encontró tipo de proveedor",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "Personalizado",
|
||||||
|
"aiProviderTypeOpenaiDescription": "API de OpenAI",
|
||||||
|
"aiProviderTypeAnthropicDescription": "API de Anthropic",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "API de Google Gemini",
|
||||||
|
"aiProviderTypeVertexAiDescription": "API de Vertex AI de Google",
|
||||||
|
"aiProviderTypeBedrockDescription": "API de Amazon Bedrock Runtime",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "API de Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "API de OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "API de Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustomDescription": "Lleve su propio endpoint o enrutamiento a través de objetivos del sitio",
|
||||||
|
"aiProviderUpstreamUrl": "URL de upstream",
|
||||||
|
"aiProviderUpstreamUrlDescription": "URL base para la API del proveedor",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Dejar en blanco para usar el URL de upstream predeterminado para este proveedor",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "URL de upstream efectivo",
|
||||||
|
"aiProviderApiKey": "Clave API",
|
||||||
|
"aiProviderApiKeyDescription": "Clave API usada para autenticar solicitudes a este proveedor",
|
||||||
|
"aiProviderCustomHeadersDescription": "Cabeceras enviadas en cada solicitud a este proveedor. Nueva línea separada: Nombre-Cabecera: valor",
|
||||||
|
"aiProviderApiKeyLastChars": "Clave API",
|
||||||
|
"aiProviderAuthType": "Tipo de autenticación",
|
||||||
|
"aiProviderAuthTypeSearch": "Buscar tipos de autenticación...",
|
||||||
|
"aiProviderAuthTypeNotFound": "No se encontró el tipo de autenticación",
|
||||||
|
"aiProviderAuthTypeBearer": "Portador",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Autorización: clave portador. Usado por OpenAI y la mayoría de los proveedores",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "Encabezado x-api-key. Usado por Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "Encabezado x-goog-api-key. Usado por Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Autorización: Clave Splunk. Usado por el recolector de eventos HTTP Splunk",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Gateway de IA de Cloudflare",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Clave Bearer. Usado por el Gateway de IA de Cloudflare",
|
||||||
|
"aiProviderAuthTypeNone": "Sin autenticación",
|
||||||
|
"aiProviderAuthTypePassthrough": "Paso a través",
|
||||||
|
"aiProviderAuthTypeDescription": "Cómo autentica solicitudes la API upstream",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Reenviar las cabeceras de clave API del solicitante al upstream",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "No enviar cabeceras de autenticación al upstream",
|
||||||
|
"aiProviderRoutingMode": "Modo de enrutamiento",
|
||||||
|
"aiProviderRoutingModeDescription": "Enviar tráfico a una URL de upstream o a objetivos HTTP en sus sitios",
|
||||||
|
"aiProviderRoutingModeUrl": "URL de upstream",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Llamar a una URL base de API pública o privada",
|
||||||
|
"aiProviderRoutingModeTarget": "Objetivos de sitio",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Ruta a través de objetivos en sus sitios",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Después de crear este proveedor, configure objetivos de sitio en la pestaña de Configuración de Red.",
|
||||||
|
"aiProviderTargetNoOne": "Este proveedor no tiene objetivos. Agregue un objetivo para enrutar solicitudes a través de sus sitios.",
|
||||||
|
"aiProviderSkipTlsVerification": "Omitir verificación de TLS",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Deshabilitar la verificación del certificado TLS para la conexión de upstream",
|
||||||
|
"aiProviderBudget": "Presupuesto",
|
||||||
|
"aiProviderBudgetDescription": "Presupuesto de gastos opcional o de token para este proveedor",
|
||||||
|
"aiProviderBudgetAmount": "Cantidad de presupuesto",
|
||||||
|
"aiProviderBudgetUnit": "Unidad de presupuesto",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Tokens",
|
||||||
|
"aiProviderEnabled": "Activado",
|
||||||
|
"aiProviderEnabledDescription": "Desactivar completamente este proveedor en todos los recursos",
|
||||||
|
"aiProviderErrorCreate": "Error al crear el proveedor de IA",
|
||||||
|
"aiProviderErrorUpdate": "Error al actualizar el proveedor de IA",
|
||||||
|
"aiProviderErrorDelete": "Error al eliminar el proveedor de IA",
|
||||||
|
"aiProviderErrorLoad": "Error al cargar el proveedor de IA",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Introduce una URL de upstream válida",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Se requiere URL de upstream para este proveedor",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Se requiere tipo de autenticación",
|
||||||
|
"aiProviderErrorApiKeyRequired": "Se requiere clave API",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "El enrutamiento de objetivos del sitio solo está disponible para proveedores personalizados",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Seleccione al menos una capacidad de API",
|
||||||
|
"aiProviderCapabilities": "Capacidades de la API",
|
||||||
|
"aiProviderCapabilitiesDescription": "Seleccione qué formatos de API puede manejar este proveedor. Los proveedores conocidos comienzan con valores predeterminados recomendados.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Seleccione qué formatos de API puede manejar este proveedor personalizado",
|
||||||
|
"aiProviderCapabilitiesSelect": "Seleccionar capacidades",
|
||||||
|
"aiProviderCapabilitiesEmpty": "No se encontraron capacidades",
|
||||||
|
"aiProviderCapabilitiesSearch": "Buscar capacidades...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Chat Completions",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Admite /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "Respuestas de OpenAI",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Admite /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Mensajes Antropicos",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Admite /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Generar contenido Gemini",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Admite la API directa de Gemini",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Invocar modelo de Bedrock",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Admite Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Generar contenido con Vertex",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Admite el formato Gemini de Vertex AI",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Predicción directa de Vertex",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Admite Vertex AI rawPredict para modelos Antropic",
|
||||||
|
"aiCapabilityBedrockConverse": "Convoque en Bedrock",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Admite la API de Amazon Bedrock Converse",
|
||||||
|
"aiProviderCreated": "Proveedor de IA creado",
|
||||||
|
"aiProviderUpdated": "Proveedor de IA actualizado",
|
||||||
|
"aiProviderDeleted": "Proveedor de IA eliminado",
|
||||||
|
"aiProviderDelete": "Eliminar proveedor",
|
||||||
|
"aiProviderDeleteConfirm": "Eliminar proveedor",
|
||||||
|
"aiProviderQuestionRemove": "¿Está seguro de que desea eliminar este proveedor de IA?",
|
||||||
|
"aiProviderMessageRemove": "Esto eliminará permanentemente el proveedor y sus modelos y objetivos. Esto no se puede deshacer.",
|
||||||
|
"aiProviderErrorNoUpdate": "El proveedor de IA no está disponible para actualizar",
|
||||||
|
"aiProviderModels": "Modelos",
|
||||||
|
"aiProviderModelsDescription": "Definir las listas de permitidos y bloqueados para este proveedor. Las solicitudes deben coincidir con una entrada permitida y no deben coincidir con una entrada bloqueada.",
|
||||||
|
"aiProviderCreateModelsDescription": "Elija qué modelos puede servir este proveedor. Una lista permitida vacía niega todo el tráfico. Puede agregar o cambiar modelos más tarde.",
|
||||||
|
"aiProviderModelsPlaceholder": "Buscar modelos o escribir una clave personalizada",
|
||||||
|
"aiProviderModelsAllow": "Lista de Permitidos",
|
||||||
|
"aiProviderModelsAllowDescription": "Modelos que pueden utilizarse a través de este proveedor. Vacío significa negar todos.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Ingrese la clave del modelo",
|
||||||
|
"aiProviderModelsAllowEmpty": "No se han agregado modelos aún.",
|
||||||
|
"aiProviderModelsBlock": "Lista de Bloqueados",
|
||||||
|
"aiProviderModelsBlockDescription": "Modelos para denegar incluso si coinciden con una entrada permitida.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Ingrese la clave del modelo",
|
||||||
|
"aiProviderModelsBlockEmpty": "No se han agregado modelos aún.",
|
||||||
|
"aiProviderModelsAdd": "Agregar Modelos",
|
||||||
|
"aiProviderModelsClearAll": "Borrar todo",
|
||||||
|
"aiProviderModelsAddCustom": "Agregar \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Presione Enter para agregar esta clave de modelo personalizada.",
|
||||||
|
"aiProviderModelsAddBulk": "Agregar {count} claves personalizadas.",
|
||||||
|
"aiProviderModelsAddBulkHint": "Presiona Enter para agregar {count} claves personalizadas.",
|
||||||
|
"aiProviderModelsAddOne": "Agregar {key} ahora",
|
||||||
|
"aiProviderModelsAddSelected": "Agregar seleccionado",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} seleccionados",
|
||||||
|
"aiProviderModelsSelectAll": "Seleccionar todo",
|
||||||
|
"aiProviderModelsClearSelected": "Limpiar",
|
||||||
|
"aiProviderModelsBulkHint": "Seleccione modelos conocidos o escriba una clave personalizada.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "No hay modelos de catálogo coincidentes.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Modelos conocidos",
|
||||||
|
"aiProviderModelsAllLabel": "Todos los modelos",
|
||||||
|
"aiProviderModelsAllPatternHint": "Comodín: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Permitir todos los modelos",
|
||||||
|
"aiProviderModelsAddAllBlock": "Bloquear todos los modelos",
|
||||||
|
"aiProviderModelsAddAllDescription": "Utiliza el comodín * para que cada clave de modelo coincida.",
|
||||||
|
"aiProviderModelsViewMore": "Ver más ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Ver menos",
|
||||||
|
"aiProviderModelsRemove": "Eliminar modelo",
|
||||||
|
"aiProviderModelsEditHint": "Haga clic para editar la configuración del modelo",
|
||||||
|
"aiProviderModelsSourceCatalog": "Modelo de catálogo conocido",
|
||||||
|
"aiProviderModelsSourceCustom": "Clave de modelo personalizada",
|
||||||
|
"aiProviderModelsSourcePattern": "Patrón de comodín",
|
||||||
|
"aiProviderModelsSourceAll": "Coincide con cada clave de modelo",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Presupuesto configurado",
|
||||||
|
"aiProviderModelsEditTitle": "Editar modelo",
|
||||||
|
"aiProviderModelsEditDescription": "Actualizar la clave del modelo o configurar su presupuesto.",
|
||||||
|
"aiProviderModelsBudgetTab": "Presupuesto",
|
||||||
|
"aiProviderModelsKeyLabel": "Clave del modelo",
|
||||||
|
"aiProviderModelsKeyRequired": "Ingrese una clave de modelo",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Esta clave de modelo ya está en una lista",
|
||||||
|
"aiProviderModelsOverlapError": "Estos patrones no pueden estar en ambas listas: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modelos actualizados",
|
||||||
|
"aiProviderModelsErrorUpdate": "No se pudo actualizar los modelos",
|
||||||
|
"aiResourceProviders": "Proveedores",
|
||||||
|
"aiResourceProvidersDescription": "Elija qué proveedores de IA puede usar este portal de IA",
|
||||||
|
"aiResourceProvidersHelp": "Adjunte proveedores y elija heredar (usar las listas de cada proveedor) o seleccionar (elija una lista de permitidos para este recurso). No se permiten patrones de permisos que entren en conflicto entre proveedores adjuntos.",
|
||||||
|
"aiResourceProvidersSelect": "Seleccionar proveedores",
|
||||||
|
"aiResourceProvidersEmpty": "No se encontraron proveedores de IA",
|
||||||
|
"aiResourceProvidersNoneAttached": "No se han adjuntado proveedores aún.",
|
||||||
|
"aiResourceProvidersAdd": "Agregar proveedor",
|
||||||
|
"aiResourceProvidersRemove": "Eliminar proveedor",
|
||||||
|
"aiResourceProviderToggleEnabled": "Habilitar o deshabilitar este proveedor en el recurso",
|
||||||
|
"aiResourceProviderDisabled": "Deshabilitado",
|
||||||
|
"aiResourceProvidersUpdated": "Proveedores actualizados",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Fallo al actualizar proveedores",
|
||||||
|
"aiResourceProviderEditDescription": "Elija cómo se exponen los modelos de este proveedor en este recurso.",
|
||||||
|
"viewProviderSettings": "Ver configuración del proveedor",
|
||||||
|
"aiResourceProviderMode": "Modo de acceso",
|
||||||
|
"aiResourceProviderModeInherit": "Heredar",
|
||||||
|
"aiResourceProviderModeSelect": "Seleccionar",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Seleccionar · {count} modelos",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Use las listas de permitidos y bloqueados de este proveedor según lo configurado en el proveedor.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Elija un subconjunto de modelos de la lista de permitidos de este proveedor para este recurso.",
|
||||||
|
"aiResourceProviderAllowModels": "Lista de permitidos",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Seleccionar modelos",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Buscar modelos...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "No se encontraron modelos",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Solo se pueden seleccionar modelos de la lista de permitidos de este proveedor.",
|
||||||
|
"aiResourceAliasRequired": "Alias es necesario para portales de IA",
|
||||||
|
"aiResourceDomainConfiguration": "Configuración del dominio",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Elija el dominio que los clientes usarán para llegar a este portal de IA.",
|
||||||
|
"aiUsageAnalyticsTitle": "Analítica de uso de IA",
|
||||||
|
"aiUsageAnalyticsDescription": "Analizar el costo del portal de IA, uso de token y actividad a través de proveedores, recursos, roles y usuarios",
|
||||||
|
"aiUsageTabOverview": "Resumen",
|
||||||
|
"aiUsageTabProviders": "Proveedores",
|
||||||
|
"aiUsageTabResources": "Recursos",
|
||||||
|
"aiUsageFilterProvider": "Proveedor",
|
||||||
|
"aiUsageFilterModel": "Modelo",
|
||||||
|
"aiUsageFilterResource": "Recurso",
|
||||||
|
"aiUsageFilterRole": "Rol",
|
||||||
|
"aiUsageFilterUser": "Usuario",
|
||||||
|
"aiUsageFilterAllProviders": "Todos los proveedores",
|
||||||
|
"aiUsageFilterAllModels": "Todos los modelos",
|
||||||
|
"aiUsageFilterAllResources": "Todos los recursos",
|
||||||
|
"aiUsageFilterAllRoles": "Todos los roles",
|
||||||
|
"aiUsageFilterAllUsers": "Todos los usuarios",
|
||||||
|
"aiUsageFilterSearch": "Buscar...",
|
||||||
|
"aiUsageFilterNotFound": "No se encontraron opciones",
|
||||||
|
"aiUsageResetFilters": "Restablecer filtros",
|
||||||
|
"aiUsageRefresh": "Actualizar",
|
||||||
|
"aiUsageTokenTypePrompt": "Indicio",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Lectura de caché",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Escritura de caché",
|
||||||
|
"aiUsageTokenTypeCompletion": "Completitud",
|
||||||
|
"aiUsageTokenTypeReasoning": "Razonamiento",
|
||||||
|
"aiUsageRequests": "Solicitudes",
|
||||||
|
"aiUsageCost": "Costo",
|
||||||
|
"aiUsageTokens": "Tokens",
|
||||||
|
"aiUsageOther": "Otro",
|
||||||
|
"aiUsageTotalRequests": "Solicitudes Totales",
|
||||||
|
"aiUsageTotalTokens": "Tokens Totales",
|
||||||
|
"aiUsageTotalCost": "Costo Total",
|
||||||
|
"aiUsageEstimated": "Estimado",
|
||||||
|
"aiUsageRequestVolume": "Volumen de Solicitudes",
|
||||||
|
"aiUsageTokenUsage": "Uso de Tokens",
|
||||||
|
"aiUsageModelCost": "Costo del Modelo",
|
||||||
|
"aiUsageModelTokens": "Tokens del Modelo",
|
||||||
|
"aiUsageTopModels": "Modelos Principales",
|
||||||
|
"aiUsageTopProviders": "Proveedores Principales",
|
||||||
|
"aiUsageProviderCost": "Costo del Proveedor",
|
||||||
|
"aiUsageProviderTokenUsage": "Uso de Tokens del Proveedor",
|
||||||
|
"aiUsageTopResources": "Recursos Principales",
|
||||||
|
"aiUsageResourceCost": "Costo del Recurso",
|
||||||
|
"aiUsageResourceTokenUsage": "Uso de Tokens del Recurso",
|
||||||
|
"aiUsageResourceTypePublic": "Recurso Público",
|
||||||
|
"aiUsageResourceTypeSite": "Recurso Privado",
|
||||||
|
"aiUsageNoResource": "Sin recurso",
|
||||||
|
"aiUsageRolesTab": "Roles",
|
||||||
|
"aiUsageUsersTab": "Usuarios",
|
||||||
|
"aiUsageTopRoles": "Roles Principales",
|
||||||
|
"aiUsageRoleCost": "Costo del Rol",
|
||||||
|
"aiUsageRoleTokenUsage": "Uso de Tokens del Rol",
|
||||||
|
"aiUsageTopUsers": "Usuarios principales",
|
||||||
|
"aiUsageUserCost": "Costo del Usuario",
|
||||||
|
"aiUsageUserTokenUsage": "Uso de Tokens del Usuario",
|
||||||
|
"aiUsageUnknownUser": "Usuario desconocido",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Claves API Virtuales",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Clave API Virtual",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Todas las Claves API Virtuales",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Claves API Virtuales Principales",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Costo de la Clave API Virtual",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Uso de Tokens de la Clave API Virtual",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Sin clave API virtual",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Clave sin nombre",
|
||||||
|
"aiUsageLoading": "Cargando...",
|
||||||
|
"aiUsageNoData": "Sin información",
|
||||||
|
"resourceBudgetSettings": "Presupuesto",
|
||||||
|
"resourceBudgetSettingsDescription": "Configure cómo este portal de IA restringe el uso en función de los límites de gasto o tokens",
|
||||||
"sidebarApiKeys": "Claves API",
|
"sidebarApiKeys": "Claves API",
|
||||||
"sidebarProvisioning": "Aprovisionamiento",
|
"sidebarProvisioning": "Aprovisionamiento",
|
||||||
"sidebarSettings": "Ajustes",
|
"sidebarSettings": "Ajustes",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Subred",
|
"subnetPlaceholder": "Subred",
|
||||||
"addressDescription": "La dirección interna del cliente. Debe estar dentro de la subred de la organización.",
|
"addressDescription": "La dirección interna del cliente. Debe estar dentro de la subred de la organización.",
|
||||||
"selectSites": "Seleccionar sitios",
|
"selectSites": "Seleccionar sitios",
|
||||||
|
"selectResources": "Seleccionar recursos",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# recurso} other {# recursos}}",
|
||||||
"selectLabels": "Seleccionar etiquetas",
|
"selectLabels": "Seleccionar etiquetas",
|
||||||
"sitesDescription": "El cliente tendrá conectividad con los sitios seleccionados",
|
"sitesDescription": "El cliente tendrá conectividad con los sitios seleccionados",
|
||||||
"clientInstallOlm": "Instalar Olm",
|
"clientInstallOlm": "Instalar Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "El tiempo está en segundos",
|
"timeIsInSeconds": "El tiempo está en segundos",
|
||||||
"requireDeviceApproval": "Requiere aprobaciones del dispositivo",
|
"requireDeviceApproval": "Requiere aprobaciones del dispositivo",
|
||||||
"requireDeviceApprovalDescription": "Los usuarios con este rol necesitan nuevos dispositivos aprobados por un administrador antes de poder conectarse y acceder a los recursos.",
|
"requireDeviceApprovalDescription": "Los usuarios con este rol necesitan nuevos dispositivos aprobados por un administrador antes de poder conectarse y acceder a los recursos.",
|
||||||
"sshSettings": "Configuración SSH",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "Portal de IA",
|
||||||
"sshAccess": "Acceso SSH",
|
"sshAccess": "Acceso SSH",
|
||||||
"rdpSettings": "Configuración RDP",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "Configuración VNC",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "Servidor SSH",
|
"sshServer": "Servidor SSH",
|
||||||
"rdpServer": "Servidor RDP",
|
"rdpServer": "Servidor RDP",
|
||||||
"vncServer": "Servidor VNC",
|
"vncServer": "Servidor VNC",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "Portal de IA",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Esquema",
|
"editInternalResourceDialogScheme": "Esquema",
|
||||||
"editInternalResourceDialogEnableSsl": "Activar TLS",
|
"editInternalResourceDialogEnableSsl": "Activar TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Permitir ping ICMP",
|
"privateResourceAllowIcmpPing": "Permitir ping ICMP",
|
||||||
"privateResourceNetworkAccess": "Acceso de red",
|
"privateResourceNetworkAccess": "Acceso de red",
|
||||||
"privateResourceNetworkAccessDescription": "Controlar el acceso a puertos TCP/UDP y si se permite ping ICMP para este recurso.",
|
"privateResourceNetworkAccessDescription": "Controlar el acceso a puertos TCP/UDP y si se permite ping ICMP para este recurso.",
|
||||||
"hostSettings": "Configuraciones del anfitrión",
|
"hostSettings": "Host",
|
||||||
"cidrSettings": "Configuraciones CIDR",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Propiedades del recurso",
|
"createInternalResourceDialogResourceProperties": "Propiedades del recurso",
|
||||||
"createInternalResourceDialogName": "Nombre",
|
"createInternalResourceDialogName": "Nombre",
|
||||||
"createInternalResourceDialogSite": "Sitio",
|
"createInternalResourceDialogSite": "Sitio",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "Portal de IA",
|
||||||
"scheme": "Esquema",
|
"scheme": "Esquema",
|
||||||
"createInternalResourceDialogScheme": "Esquema",
|
"createInternalResourceDialogScheme": "Esquema",
|
||||||
"createInternalResourceDialogEnableSsl": "Activar TLS",
|
"createInternalResourceDialogEnableSsl": "Activar TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Contraseña válida",
|
"validPassword": "Contraseña válida",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Clave API Virtual Válida",
|
||||||
"view": "Ver",
|
"view": "Ver",
|
||||||
"configManaged": "Configuración Gestionada",
|
"configManaged": "Configuración Gestionada",
|
||||||
"connectedClient": "Cliente conectado",
|
"connectedClient": "Cliente conectado",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Retención de Log",
|
"logRetention": "Retención de Log",
|
||||||
"logRetentionDescription": "Administrar cuánto tiempo se conservan los diferentes tipos de registros para esta organización o desactivarlos",
|
"logRetentionDescription": "Administrar cuánto tiempo se conservan los diferentes tipos de registros para esta organización o desactivarlos",
|
||||||
"requestLogsDescription": "Ver registros de solicitudes detallados para los recursos de esta organización",
|
"requestLogsDescription": "Ver registros de solicitudes detallados para los recursos de esta organización",
|
||||||
|
"aiSessionLogs": "Registros de Sesiones del Portal de IA",
|
||||||
|
"aiSessionLogsDescription": "Ver transcripciones de solicitud y respuesta para solicitudes del portal de IA en esta organización",
|
||||||
|
"sidebarLogsAi": "Registros de Sesiones",
|
||||||
|
"commandLogsAi": "Registros de Sesiones",
|
||||||
|
"sidebarLogsAiUsage": "Analítica de Uso",
|
||||||
|
"commandLogsAiUsage": "Analítica de Uso",
|
||||||
|
"provider": "Proveedor",
|
||||||
|
"capability": "Capacidad",
|
||||||
|
"model": "Modelo",
|
||||||
|
"virtualApiKey": "Clave API Virtual",
|
||||||
|
"noVirtualApiKey": "Sin clave API virtual",
|
||||||
|
"stream": "Flujo",
|
||||||
|
"streaming": "Flujo de eventos",
|
||||||
|
"nonStreaming": "No-flujo",
|
||||||
|
"statusCode": "Código de estado",
|
||||||
|
"aiSessionId": "ID de Sesión",
|
||||||
|
"aiSessionRequest": "Solicitud",
|
||||||
|
"aiSessionResponse": "Respuesta",
|
||||||
|
"aiSessionNoData": "Sin datos capturados",
|
||||||
|
"aiSessionCouldNotParse": "(sin procesar, no se pudo analizar la transcripción)",
|
||||||
|
"aiSessionLogTruncated": "Esta sesión fue truncada antes del almacenamiento y puede estar incompleta.",
|
||||||
|
"aiSessionViewRaw": "Ver JSON sin procesar",
|
||||||
|
"aiSessionViewChat": "Ver Chat",
|
||||||
|
"cost": "Costo",
|
||||||
|
"estimated": "Estimado",
|
||||||
|
"tokenUsage": "Uso de Tokens",
|
||||||
|
"promptTokens": "Tokens de Indicio",
|
||||||
|
"cacheReadTokens": "Tokens de Lectura de Caché",
|
||||||
|
"cacheWriteTokens": "Tokens de Escritura de Caché",
|
||||||
|
"completionTokens": "Tokens de Completitud",
|
||||||
|
"reasoningTokens": "Tokens de Razonamiento",
|
||||||
|
"totalTokens": "Total de Tokens",
|
||||||
"requestAnalyticsDescription": "Ver análisis de solicitudes detalladas de recursos en esta organización",
|
"requestAnalyticsDescription": "Ver análisis de solicitudes detalladas de recursos en esta organización",
|
||||||
"logRetentionRequestLabel": "Retención de Registro de Solicitud HTTP",
|
"logRetentionRequestLabel": "Retención de Registro de Solicitud HTTP",
|
||||||
"logRetentionRequestDescription": "Cuánto tiempo conservar los registros de solicitudes",
|
"logRetentionRequestDescription": "Cuánto tiempo conservar los registros de solicitudes",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Cuánto tiempo retener los registros de acción",
|
"logRetentionActionDescription": "Cuánto tiempo retener los registros de acción",
|
||||||
"logRetentionConnectionLabel": "Retención de Registro de Conexión",
|
"logRetentionConnectionLabel": "Retención de Registro de Conexión",
|
||||||
"logRetentionConnectionDescription": "Cuánto tiempo conservar los registros de conexión",
|
"logRetentionConnectionDescription": "Cuánto tiempo conservar los registros de conexión",
|
||||||
|
"logRetentionAISessionsLabel": "Retención de Registro de Sesiones del Portal de IA",
|
||||||
|
"logRetentionAISessionsDescription": "Cuánto tiempo retener los registros de sesión de solicitud/respuesta del portal de IA",
|
||||||
"logRetentionDisabled": "Deshabilitado",
|
"logRetentionDisabled": "Deshabilitado",
|
||||||
"logRetention3Days": "3 días",
|
"logRetention3Days": "3 días",
|
||||||
"logRetention7Days": "7 días",
|
"logRetention7Days": "7 días",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Dirección de origen",
|
"sourceAddress": "Dirección de origen",
|
||||||
"destinationAddress": "Dirección de destino",
|
"destinationAddress": "Dirección de destino",
|
||||||
"duration": "Duración",
|
"duration": "Duración",
|
||||||
"licenseRequiredToUse": "Se requiere una licencia <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> para usar esta función. <bookADemoLink>Reserve una demostración gratuita o una prueba POC para saber más.</bookADemoLink>",
|
"licenseRequiredToUse": "Se requiere una licencia para la <enterpriseEditionLink>Edición Empresarial</enterpriseEditionLink>. <bookADemoLink>Reserve una demostración o prueba</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "La <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> es necesaria para utilizar esta función. Esta función también está disponible en <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Reserva una demostración gratuita o prueba POC para saber más.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "Se requiere una licencia para la <enterpriseEditionLink>Edición Empresarial</enterpriseEditionLink>. <bookADemoLink>Reserve una demostración o prueba</bookADemoLink>",
|
||||||
"certResolver": "Resolver certificado",
|
"certResolver": "Resolver certificado",
|
||||||
"certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.",
|
"certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.",
|
||||||
"selectCertResolver": "Seleccionar Resolver Certificado",
|
"selectCertResolver": "Seleccionar Resolver Certificado",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Seleccionar múltiples sitios habilita el enrutamiento resistente y la conmutación por error para alta disponibilidad.",
|
"internalResourceFormMultiSiteRoutingHelp": "Seleccionar múltiples sitios habilita el enrutamiento resistente y la conmutación por error para alta disponibilidad.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Más información",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Más información",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Restringir el acceso a puertos TCP/UDP específicos o permitir/bloquear todos los puertos.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Restringir el acceso a puertos TCP/UDP específicos o permitir/bloquear todos los puertos.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "Configuración HTTP",
|
"createInternalResourceDialogHttpConfiguration": "Configuración del dominio",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Elija el dominio que los clientes usarán para alcanzar este recurso a través de HTTP o HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Elija el dominio que los clientes usarán para alcanzar este recurso a través de HTTP o HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "Configuración HTTP",
|
"editInternalResourceDialogHttpConfiguration": "Configuración del dominio",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Elija el dominio que los clientes usarán para alcanzar este recurso a través de HTTP o HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Elija el dominio que los clientes usarán para alcanzar este recurso a través de HTTP o HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Sin Etiqueta",
|
"resourceLauncherUnlabeled": "Sin Etiqueta",
|
||||||
|
"resourceLauncherAiGateway": "Portal de IA",
|
||||||
"resourceLauncherNoSite": "Sin Sitio",
|
"resourceLauncherNoSite": "Sin Sitio",
|
||||||
|
"resourceLauncherAvailableModels": "Modelos Disponibles",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modelos que puede usar con este portal de IA.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "No hay modelos disponibles para este recurso.",
|
||||||
|
"resourceLauncherAvailableModelsError": "No se pudieron cargar los modelos disponibles.",
|
||||||
|
"resourceLauncherApiKeys": "Claves API",
|
||||||
|
"resourceLauncherApiKeysDescription": "Use su clave de identidad o una clave atribuida para autenticarse con este recurso.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Clave de Identidad",
|
||||||
|
"resourceLauncherApiKeysManual": "Claves Atribuidas",
|
||||||
|
"resourceLauncherApiKeysEmpty": "No hay claves API disponibles para este recurso.",
|
||||||
|
"resourceLauncherApiKeysError": "No se pudieron cargar las claves API.",
|
||||||
"resourceLauncherNoResourcesInGroup": "No hay recursos en este grupo",
|
"resourceLauncherNoResourcesInGroup": "No hay recursos en este grupo",
|
||||||
"resourceLauncherEmptyStateTitle": "No hay Recursos Disponibles",
|
"resourceLauncherEmptyStateTitle": "No hay Recursos Disponibles",
|
||||||
"resourceLauncherEmptyStateDescription": "Todavía no tienes acceso a ningún recurso. Contacta a tu administrador para solicitar acceso.",
|
"resourceLauncherEmptyStateDescription": "Todavía no tienes acceso a ningún recurso. Contacta a tu administrador para solicitar acceso.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "No se pudo eliminar la vista del lanzador. Por favor, intenta de nuevo.",
|
"resourceLauncherViewDeleteFailedDescription": "No se pudo eliminar la vista del lanzador. Por favor, intenta de nuevo.",
|
||||||
"memberPortalPrevious": "Anterior",
|
"memberPortalPrevious": "Anterior",
|
||||||
"memberPortalNext": "Siguiente",
|
"memberPortalNext": "Siguiente",
|
||||||
"httpSettings": "Configuración HTTP",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "Configuración TCP",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "Configuración UDP",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Estableciendo una conexión segura…",
|
"sshConnectingDescription": "Estableciendo una conexión segura…",
|
||||||
"sshConnecting": "Conectando…",
|
"sshConnecting": "Conectando…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Veuillez vous assurer que vous êtes connecté avec le bon compte.",
|
"inviteLoginUser": "Veuillez vous assurer que vous êtes connecté avec le bon compte.",
|
||||||
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder ne concerne pas un utilisateur existant.",
|
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation à laquelle vous essayez d'accéder ne concerne pas un utilisateur existant.",
|
||||||
"inviteCreateUser": "Veuillez d'abord créer un compte.",
|
"inviteCreateUser": "Veuillez d'abord créer un compte.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Les invitations ne peuvent être acceptées que par des comptes internes. Déconnectez-vous et connectez-vous avec votre mot de passe pour cet email.",
|
||||||
|
"inviteLoginInternalOnly": "Les invitations nécessitent un compte interne avec un mot de passe. Créez un compte ou connectez-vous avec votre mot de passe.",
|
||||||
"goHome": "Retour à l'accueil",
|
"goHome": "Retour à l'accueil",
|
||||||
"inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent",
|
"inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent",
|
||||||
"createAnAccount": "Créer un compte",
|
"createAnAccount": "Créer un compte",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Aucun pays trouvé.",
|
"noCountryFound": "Aucun pays trouvé.",
|
||||||
"siteSelectionDescription": "Ce site fournira la connectivité à la cible.",
|
"siteSelectionDescription": "Ce site fournira la connectivité à la cible.",
|
||||||
"resourceType": "Type de ressource",
|
"resourceType": "Type de ressource",
|
||||||
"resourceTypeDescription": "Cela contrôle le protocole de la ressource et comment il sera rendu dans le navigateur. Cela ne peut pas être changé plus tard.",
|
"resourceTypeDescription": "Cela contrôle le protocole de ressource et ne peut pas être modifié ultérieurement.",
|
||||||
|
"resourceTypeSearch": "Rechercher des types de ressource...",
|
||||||
|
"resourceTypeNotFound": "Aucun type de ressource trouvé",
|
||||||
|
"resourceTypeHttpDescription": "Proxy du trafic web via HTTPS en utilisant un nom de domaine",
|
||||||
|
"resourceTypeInferenceDescription": "Acheminer les requêtes AI via les fournisseurs attachés",
|
||||||
|
"resourceTypeSshDescription": "Accéder à un serveur SSH depuis le navigateur",
|
||||||
|
"resourceTypeRdpDescription": "Accéder à un bureau distant depuis le navigateur",
|
||||||
|
"resourceTypeVncDescription": "Accéder à un bureau VNC depuis le navigateur",
|
||||||
|
"resourceTypeTcpDescription": "Proxy du trafic TCP brut utilisant un numéro de port",
|
||||||
|
"resourceTypeUdpDescription": "Proxy du trafic UDP brut utilisant un numéro de port",
|
||||||
|
"privateResourceTypeDescription": "Cela contrôle comment les clients atteignent la ressource. Cela ne peut pas être modifié plus tard.",
|
||||||
|
"privateResourceTypeHostDescription": "Exposer un hôte unique sur le réseau du site aux clients connectés",
|
||||||
|
"privateResourceTypeCidrDescription": "Exposer une plage CIDR sur le réseau du site aux clients connectés",
|
||||||
|
"privateResourceTypeHttpDescription": "Accéder à un service HTTP ou HTTPS via un domaine",
|
||||||
|
"privateResourceTypeSshDescription": "Accéder à un serveur SSH depuis les clients connectés",
|
||||||
"resourceDomainDescription": "La ressource sera servie à ce nom de domaine pleinement qualifié.",
|
"resourceDomainDescription": "La ressource sera servie à ce nom de domaine pleinement qualifié.",
|
||||||
"resourceHTTPSSettings": "Paramètres HTTPS",
|
"resourceHTTPSSettings": "Paramètres HTTPS",
|
||||||
"resourceHTTPSSettingsDescription": "Configurer comment la ressource sera accédée via HTTPS",
|
"resourceHTTPSSettingsDescription": "Configurer comment la ressource sera accédée via HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Enregistrer les paramètres",
|
"authMethodsSave": "Enregistrer les paramètres",
|
||||||
"policyAuthStackTitle": "Authentification",
|
"policyAuthStackTitle": "Authentification",
|
||||||
"policyAuthStackDescription": "Contrôlez quelles méthodes d'authentification sont nécessaires pour accéder à cette ressource",
|
"policyAuthStackDescription": "Contrôlez quelles méthodes d'authentification sont nécessaires pour accéder à cette ressource",
|
||||||
|
"policyAuthInferenceStackDescription": "Choisissez quels utilisateurs et rôles peuvent s'authentifier à cette passerelle AI",
|
||||||
"policyAuthOrLogicTitle": "Plusieurs méthodes d'authentification actives",
|
"policyAuthOrLogicTitle": "Plusieurs méthodes d'authentification actives",
|
||||||
"policyAuthOrLogicBanner": "Les visiteurs peuvent s'authentifier en utilisant l'une des méthodes actives ci-dessous. Ils n'ont pas besoin de toutes les compléter.",
|
"policyAuthOrLogicBanner": "Les visiteurs peuvent s'authentifier en utilisant l'une des méthodes actives ci-dessous. Ils n'ont pas besoin de toutes les compléter.",
|
||||||
"policyAuthMethodActive": "Actif",
|
"policyAuthMethodActive": "Actif",
|
||||||
"policyAuthMethodOff": "Éteint",
|
"policyAuthMethodOff": "Éteint",
|
||||||
"policyAuthSsoTitle": "SSO de la plateforme",
|
"policyAuthSsoTitle": "SSO de la plateforme",
|
||||||
"policyAuthSsoDescription": "Exigez une connexion via le fournisseur d'identité de votre organisation",
|
"policyAuthSsoDescription": "Exigez une connexion via le fournisseur d'identité de votre organisation",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "URL de connexion",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Chaque utilisateur a déjà une clé API d'identité, vous devez donc uniquement créer des clés API virtuelles pour les clients non-utilisateurs ou l'accès partagé. Les utilisateurs peuvent récupérer leur clé en se connectant avec leur fournisseur d'identité à l'URL de cette ressource, où elle sera affichée après la connexion.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Chaque utilisateur a déjà une clé API d'identité, vous devez donc uniquement créer des clés API virtuelles pour les clients non-utilisateurs ou l'accès partagé. Les utilisateurs récupèrent leur clé ici après s'être connectés avec leur fournisseur d'identité.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} utilisateurs, {roles} rôles",
|
"policyAuthSsoSummary": "{idp} · {users} utilisateurs, {roles} rôles",
|
||||||
"policyAuthSsoDefaultIdp": "Fournisseur par défaut",
|
"policyAuthSsoDefaultIdp": "Fournisseur par défaut",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Ajouter un fournisseur d'identité par défaut",
|
"policyAuthAddDefaultIdentityProvider": "Ajouter un fournisseur d'identité par défaut",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Un nouveau rôle est requis",
|
"accessRoleErrorNewRequired": "Un nouveau rôle est requis",
|
||||||
"accessRoleErrorRemove": "Échec de la suppression du rôle",
|
"accessRoleErrorRemove": "Échec de la suppression du rôle",
|
||||||
"accessRoleErrorRemoveDescription": "Une erreur s'est produite lors de la suppression du rôle.",
|
"accessRoleErrorRemoveDescription": "Une erreur s'est produite lors de la suppression du rôle.",
|
||||||
|
"accessRoleInferenceBudget": "Budget AI",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Configurez comment les membres de ce rôle restreignent l'utilisation de l'IA en fonction des dépenses ou des limites de jetons",
|
||||||
"accessRoleName": "Nom du rôle",
|
"accessRoleName": "Nom du rôle",
|
||||||
"accessRoleQuestionRemove": "Vous êtes sur le point de supprimer le rôle `{name}`. Vous ne pouvez pas annuler cette action.",
|
"accessRoleQuestionRemove": "Vous êtes sur le point de supprimer le rôle `{name}`. Vous ne pouvez pas annuler cette action.",
|
||||||
"accessRoleRemove": "Supprimer le rôle",
|
"accessRoleRemove": "Supprimer le rôle",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Supprimer un site",
|
"actionDeleteSite": "Supprimer un site",
|
||||||
"actionGetSite": "Obtenir un site",
|
"actionGetSite": "Obtenir un site",
|
||||||
"actionListSites": "Lister les sites",
|
"actionListSites": "Lister les sites",
|
||||||
|
"actionCreateAiProvider": "Créer un fournisseur AI",
|
||||||
|
"actionDeleteAiProvider": "Supprimer un fournisseur AI",
|
||||||
|
"actionGetAiProvider": "Obtenir un fournisseur AI",
|
||||||
|
"actionListAiProviders": "Lister les fournisseurs AI",
|
||||||
|
"actionUpdateAiProvider": "Mettre à jour un fournisseur AI",
|
||||||
|
"actionCreateAiModel": "Créer un modèle AI",
|
||||||
|
"actionDeleteAiModel": "Supprimer un modèle AI",
|
||||||
|
"actionGetAiModel": "Obtenir un modèle AI",
|
||||||
|
"actionListAiModels": "Lister les modèles AI",
|
||||||
|
"actionUpdateAiModel": "Mettre à jour un modèle AI",
|
||||||
|
"actionCreateAiBudget": "Créer un budget AI",
|
||||||
|
"actionDeleteAiBudget": "Supprimer un budget AI",
|
||||||
|
"actionGetAiBudget": "Obtenir un budget AI",
|
||||||
|
"actionListAiBudgets": "Lister les budgets AI",
|
||||||
|
"actionUpdateAiBudget": "Mettre à jour un budget AI",
|
||||||
|
"actionListResourceAiModels": "Lister les modèles AI de ressource",
|
||||||
|
"actionSetResourceAiModels": "Définir les modèles AI de ressource",
|
||||||
|
"actionCreateVirtualApiKey": "Créer une clé API virtuelle",
|
||||||
|
"actionDeleteVirtualApiKey": "Supprimer une clé API virtuelle",
|
||||||
|
"actionGetVirtualApiKey": "Obtenir une clé API virtuelle",
|
||||||
|
"actionListVirtualApiKeys": "Lister les clés API virtuelles",
|
||||||
|
"actionUpdateVirtualApiKey": "Mettre à jour une clé API virtuelle",
|
||||||
"actionApplyBlueprint": "Appliquer la Config",
|
"actionApplyBlueprint": "Appliquer la Config",
|
||||||
"actionListBlueprints": "Lister les plans",
|
"actionListBlueprints": "Lister les plans",
|
||||||
"actionGetBlueprint": "Obtenez un plan",
|
"actionGetBlueprint": "Obtenez un plan",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Définir le code PIN de la ressource",
|
"actionSetResourcePincode": "Définir le code PIN de la ressource",
|
||||||
"actionSetResourceEmailWhitelist": "Définir la liste blanche des emails de la ressource",
|
"actionSetResourceEmailWhitelist": "Définir la liste blanche des emails de la ressource",
|
||||||
"actionGetResourceEmailWhitelist": "Obtenir la liste blanche des emails de la ressource",
|
"actionGetResourceEmailWhitelist": "Obtenir la liste blanche des emails de la ressource",
|
||||||
|
"actionListResourcePolicies": "Lister les politiques de ressource",
|
||||||
|
"actionCreateResourcePolicy": "Créer une politique de ressource",
|
||||||
"actionGetResourcePolicy": "Obtenir une politique de ressources",
|
"actionGetResourcePolicy": "Obtenir une politique de ressources",
|
||||||
"actionUpdateResourcePolicy": "Mettre à jour la politique de ressources",
|
"actionUpdateResourcePolicy": "Mettre à jour la politique de ressources",
|
||||||
|
"actionDeleteResourcePolicy": "Supprimer une politique de ressource",
|
||||||
"actionSetResourcePolicyUsers": "Définir les utilisateurs de la politique de ressources",
|
"actionSetResourcePolicyUsers": "Définir les utilisateurs de la politique de ressources",
|
||||||
"actionSetResourcePolicyRoles": "Définir les rôles de la politique de ressources",
|
"actionSetResourcePolicyRoles": "Définir les rôles de la politique de ressources",
|
||||||
"actionSetResourcePolicyPassword": "Définir le mot de passe de la politique de ressources",
|
"actionSetResourcePolicyPassword": "Définir le mot de passe de la politique de ressources",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Invitations",
|
"sidebarInvitations": "Invitations",
|
||||||
"sidebarRoles": "Rôles",
|
"sidebarRoles": "Rôles",
|
||||||
"sidebarShareableLinks": "Liens partageables",
|
"sidebarShareableLinks": "Liens partageables",
|
||||||
|
"sidebarAiGateway": "Passerelle AI",
|
||||||
|
"sidebarAiProviders": "Fournisseurs",
|
||||||
|
"commandAiProviders": "Fournisseurs AI",
|
||||||
|
"sidebarVirtualApiKeys": "Clés API virtuelles",
|
||||||
|
"sidebarMyApiKeys": "Vos clés API",
|
||||||
|
"sidebarAccount": "Lanceur",
|
||||||
|
"commandVirtualApiKeys": "Clés API virtuelles",
|
||||||
|
"virtualApiKeysTitle": "Gérer les clés API virtuelles",
|
||||||
|
"virtualApiKeysDescription": "Créer et gérer des clés API manuelles pour l'accès à la passerelle AI publique",
|
||||||
|
"virtualApiKeysTabIdentity": "Clés d'identité",
|
||||||
|
"virtualApiKeysTabVirtual": "Clés virtuelles",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Clés d'identité pour chaque utilisateur",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Chaque utilisateur a déjà une clé d'identité Pangolin pour cette organisation. Elle est unique à leur compte et les authentifie aux passerelles AI qu'ils peuvent utiliser.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Exemple",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Comment les utilisateurs obtiennent leur clé",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Visitez une URL de passerelle AI publique dans le navigateur et connectez-vous avec leur compte.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Ou accédez à <url></url> en étant connecté.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Vous pouvez créer des clés API virtuelles supplémentaires dans l'onglet Clés Virtuelles. Ces clés peuvent être délimitées à des passerelles AI publiques spécifiques et éventuellement associées à un utilisateur. Créer une clé accorde immédiatement l'accès aux passerelles AI publiques sélectionnées.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Créer manuellement une clé",
|
||||||
|
"virtualApiKeysEmailIdentity": "Envoyer les clés d'identité par email",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Envoyez à chaque utilisateur sélectionné ou rôle leur clé d'identité Pangolin.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Envoyer à tous les utilisateurs",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Envoyez un email à chaque membre de l'organisation possédant une adresse email de compte.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Utilisateurs",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Rôles",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Envoyer des emails",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Clés d'identité envoyées par email",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "{skipped} utilisateurs ont été ignorés car ils n'ont pas d'adresse email.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "Sélectionnez au moins un utilisateur ou rôle, ou envoyez à tous les utilisateurs.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Erreur lors de l'envoi des clés d'identité",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Échec de l'envoi des clés d'identité par email",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Clés d'identité pour chaque utilisateur",
|
||||||
|
"virtualApiKeysBannerTitle": "Tous les utilisateurs ont déjà une clé d'identité disponible à {keysUrl}. Vous pouvez également générer manuellement des clés ici pour accorder un accès direct aux passerelles AI publiques.",
|
||||||
|
"virtualApiKeysBannerDescription": "Voir les clés d'identité",
|
||||||
|
"virtualApiKeysBannerButtonText": "Voir les clés d'identité",
|
||||||
|
"virtualApiKeys": "Clés API virtuelles",
|
||||||
|
"virtualApiKeysSearch": "Rechercher des clés...",
|
||||||
|
"virtualApiKeysCreate": "Émettre une clé manuelle pouvant appeler les passerelles AI publiques dans cette organisation",
|
||||||
|
"virtualApiKeysCreateDescription": "Créer une clé",
|
||||||
|
"virtualApiKeysCreateButton": "Créer une clé",
|
||||||
|
"virtualApiKeysEmpty": "Nom",
|
||||||
|
"virtualApiKeysName": "Nom",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Associer un utilisateur (facultatif)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Associer un utilisateur (facultatif)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Toutes les passerelles AI publiques",
|
||||||
|
"virtualApiKeysAllResources": "Permettre à cette clé d'accéder à chaque passerelle AI publique de l'organisation",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Passerelles AI publiques",
|
||||||
|
"virtualApiKeysSelectResources": "Sélectionner des ressources",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Sélectionner des ressources",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Aucune ressource",
|
||||||
|
"virtualApiKeysNoResources": "Clé",
|
||||||
|
"virtualApiKeysSecret": "Clé",
|
||||||
|
"virtualApiKeysCopyKey": "Voir le secret",
|
||||||
|
"virtualApiKeysViewSecret": "Voir le secret",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Cet accès secret accorde l'accès aux passerelles AI publiques assignées à cette clé",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Modifier la clé API virtuelle",
|
||||||
|
"virtualApiKeysEdit": "Mettre à jour l'utilisateur associé et l'accès à la passerelle AI publique pour cette clé",
|
||||||
|
"virtualApiKeysEditDescription": "Enregistrer les modifications",
|
||||||
|
"virtualApiKeysSaveButton": "Sélectionnez au moins une passerelle AI publique, ou activez toutes les passerelles AI publiques",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Clé API virtuelle mise à jour",
|
||||||
|
"virtualApiKeysUpdated": "La clé API virtuelle a été mise à jour",
|
||||||
|
"virtualApiKeysUpdatedDescription": "Erreur lors de la mise à jour de la clé API virtuelle",
|
||||||
|
"virtualApiKeysErrorUpdate": "Échec de la mise à jour de la clé API virtuelle",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Erreur lors de la création de la clé API virtuelle",
|
||||||
|
"virtualApiKeysErrorCreate": "Échec de la création de la clé API virtuelle",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Erreur lors de la suppression de la clé API virtuelle",
|
||||||
|
"virtualApiKeysErrorDelete": "Échec de la suppression de la clé API virtuelle",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Clé API virtuelle supprimée",
|
||||||
|
"virtualApiKeysDeleted": "La clé API virtuelle a été supprimée",
|
||||||
|
"virtualApiKeysDeletedDescription": "Supprimer la clé API virtuelle",
|
||||||
|
"virtualApiKeysDelete": "Supprimer la clé",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Supprimer la clé",
|
||||||
|
"virtualApiKeysQuestionRemove": "Êtes-vous sûr de vouloir supprimer cette clé API virtuelle ?",
|
||||||
|
"virtualApiKeysMessageRemove": "Erreur lors du chargement du secret",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Erreur lors du chargement du secret",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Non assigné",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Budget",
|
||||||
|
"virtualApiKeysInferenceBudget": "Budget",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Email de la clé lors de la génération",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Envoyer cette clé par email",
|
||||||
|
"virtualApiKeysEmailThisKey": "Envoyer la clé à l'utilisateur associé et à des adresses supplémentaires après sa création",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Envoyez la clé actuelle à l'utilisateur associé et à des adresses supplémentaires",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "L'email n'est pas configuré sur ce serveur",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "L'Email n'est pas configuré sur ce serveur",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Envoyer à l'utilisateur associé",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Envoyez la clé à l'adresse email du compte de l'utilisateur associé",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Associez un utilisateur pour envoyer la clé à cet utilisateur",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Emails supplémentaires",
|
||||||
|
"virtualApiKeysEmailAdditional": "Ajoutez un email et appuyez sur Entrée",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Sélectionnez l'utilisateur associé ou ajoutez au moins une adresse email",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Vos clés API",
|
||||||
|
"myVirtualApiKeysTitle": "Voyez votre clé d'identité et toutes les clés API virtuelles attribuées à vous dans cette organisation",
|
||||||
|
"myVirtualApiKeysDescription": "Vos clés API pour {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Voyez votre clé d'identité et les clés API virtuelles attribuées à vous qui peuvent accéder à {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Clé d'identité",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Clé d'identité",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Votre clé API personnelle",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Votre clé API personnelle pour {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Votre clé personnelle pour cette organisation. Utilisez-la pour appeler {resourceName}.",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Clés attribuées",
|
||||||
|
"myVirtualApiKeysManualTitle": "Clés attribuées",
|
||||||
|
"myVirtualApiKeysManualDescription": "Clés API virtuelles manuelles associées à votre compte qui peuvent accéder à {resourceName}",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Pas encore de clés attribuées",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Identité",
|
||||||
|
"myVirtualApiKeysKindUser": "Manuel",
|
||||||
|
"myVirtualApiKeysKindManual": "Clé non nommée",
|
||||||
|
"myVirtualApiKeysUnnamed": "Révéler le secret",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Ce secret vous authentifie aux ressources de la passerelle AI",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Fournisseurs AI",
|
||||||
|
"aiProvidersTitle": "Connectez des fournisseurs de modèles pour des charges de travail AI dans cette organisation",
|
||||||
|
"aiProvidersDescription": "Connecter les fournisseurs de modèles",
|
||||||
|
"aiProvidersBannerTitle": "Les fournisseurs sont les backends de modèle que Pangolin utilise pour les charges de travail AI. Connectez OpenAI, Anthropic et d'autres fournisseurs ici, puis attachez-les aux passerelles AI publiques afin que les utilisateurs puissent appeler des modèles avec un accès conscient de l'identité, des budgets et des journaux.",
|
||||||
|
"aiProvidersBannerDescription": "Ajouter un fournisseur",
|
||||||
|
"aiProvidersAdd": "Rechercher des fournisseurs...",
|
||||||
|
"aiProvidersSearch": "Aucun fournisseur AI pour le moment",
|
||||||
|
"aiProvidersEmpty": "Créer un fournisseur AI",
|
||||||
|
"aiProviderCreate": "Ajouter un fournisseur de modèles pour cette organisation",
|
||||||
|
"aiProviderCreateDescription": "Voir tous les fournisseurs",
|
||||||
|
"aiProviderSeeAll": "Paramètres du fournisseur pour {providerName}",
|
||||||
|
"aiProviderSetting": "Configurez ce fournisseur AI",
|
||||||
|
"aiProviderSettingDescription": "Général",
|
||||||
|
"aiProviderGeneral": "Paramètres de base pour ce fournisseur",
|
||||||
|
"aiProviderGeneralDescription": "Configuration",
|
||||||
|
"aiProviderConfiguration": "Routage du réseau et authentification pour ce fournisseur",
|
||||||
|
"aiProviderConfigurationDescription": "Paramètres du réseau",
|
||||||
|
"aiProviderNetworkSettings": "Choisissez comment le trafic atteint ce fournisseur",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Authentification",
|
||||||
|
"aiProviderAuthSettings": "Authentification",
|
||||||
|
"aiProviderAuthSettingsDescription": "Budget",
|
||||||
|
"aiProviderBudgetSettings": "Configurez comment ce fournisseur restreint l'utilisation en fonction des dépenses ou des limites de jetons",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Ajouter un budget",
|
||||||
|
"aiBudgetAdd": "Ajouter un budget",
|
||||||
|
"aiBudgetEmpty": "Type de dépense",
|
||||||
|
"aiBudgetUnit": "Période de réinitialisation",
|
||||||
|
"aiBudgetPeriod": "Dépense maximale",
|
||||||
|
"aiBudgetAmount": "Dépense maximale",
|
||||||
|
"aiBudgetAmountPlaceholder": "Horaire",
|
||||||
|
"aiBudgetPeriodHourly": "Quotidien",
|
||||||
|
"aiBudgetPeriodDaily": "Hebdomadaire",
|
||||||
|
"aiBudgetPeriodWeekly": "Mensuel",
|
||||||
|
"aiBudgetPeriodMonthly": "Annuel",
|
||||||
|
"aiBudgetPeriodYearly": "Durée de vie",
|
||||||
|
"aiBudgetPeriodLifetime": "Durée de vie",
|
||||||
|
"aiBudgetUnitUsd": "Jetons",
|
||||||
|
"aiBudgetUnitTokens": "Un budget pour cette période de réinitialisation et ce type de dépense existe déjà",
|
||||||
|
"aiBudgetConflictError": "Entrez une dépense maximale supérieure à 0",
|
||||||
|
"aiBudgetInvalidAmountError": "Entrez une dépense maximale supérieure à 0",
|
||||||
|
"aiBudgetUpdated": "Échec de la mise à jour des budgets",
|
||||||
|
"aiBudgetErrorSave": "Type de fournisseur",
|
||||||
|
"aiProviderType": "Type de fournisseur",
|
||||||
|
"aiProviderTypeSearch": "Rechercher des fournisseurs...",
|
||||||
|
"aiProviderTypeNotFound": "OpenAI",
|
||||||
|
"aiProviderTypeOpenai": "Anthropic",
|
||||||
|
"aiProviderTypeAnthropic": "Google Gemini",
|
||||||
|
"aiProviderTypeGoogleGemini": "Vertex AI",
|
||||||
|
"aiProviderTypeVertexAi": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeBedrock": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "OpenRouter",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Personnalisé",
|
||||||
|
"aiProviderTypeCustom": "Personnalisé",
|
||||||
|
"aiProviderTypeOpenaiDescription": "API Anthropic",
|
||||||
|
"aiProviderTypeAnthropicDescription": "API Google Gemini",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "API Google Vertex AI",
|
||||||
|
"aiProviderTypeVertexAiDescription": "API d'exécution Amazon Bedrock",
|
||||||
|
"aiProviderTypeBedrockDescription": "API Microsoft Foundry",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "API OpenRouter",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "API Passerelle Vercel AI",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "API de passerelle AI Vercel",
|
||||||
|
"aiProviderTypeCustomDescription": "Apportez votre propre point de terminaison ou routez via les cibles du site",
|
||||||
|
"aiProviderUpstreamUrl": "URL amont",
|
||||||
|
"aiProviderUpstreamUrlDescription": "URL de base pour l'API du fournisseur",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Laissez vide pour utiliser l'URL amont par défaut pour ce fournisseur",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "URL amont effective",
|
||||||
|
"aiProviderApiKey": "Clé API",
|
||||||
|
"aiProviderApiKeyDescription": "Clé API utilisée pour authentifier les requêtes auprès de ce fournisseur",
|
||||||
|
"aiProviderCustomHeadersDescription": "En-têtes envoyés à chaque requête à ce fournisseur. Séparé par une nouvelle ligne : Nom-de-l'en-tête: valeur",
|
||||||
|
"aiProviderApiKeyLastChars": "Clé API",
|
||||||
|
"aiProviderAuthType": "Type d'authentification",
|
||||||
|
"aiProviderAuthTypeSearch": "Rechercher des types d'authentification...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Aucun type d'authentification trouvé",
|
||||||
|
"aiProviderAuthTypeBearer": "Porteur",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Authorization : Clé Bearer. Utilisé par OpenAI et la plupart des fournisseurs",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "En-tête x-api-key. Utilisé par Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "En-tête x-goog-api-key. Utilisé par Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Authorization : Clé Splunk. Utilisé par le collecteur d'événements HTTP Splunk",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Passerelle AI de Cloudflare",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization : Clé Bearer. Utilisé par la passerelle AI de Cloudflare",
|
||||||
|
"aiProviderAuthTypeNone": "Pas d'authentification",
|
||||||
|
"aiProviderAuthTypePassthrough": "Transparence",
|
||||||
|
"aiProviderAuthTypeDescription": "Comment l'API amont authentifie les requêtes",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Transmettez les en-têtes de clé API de l'appelant vers l'amont",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Ne pas envoyer d'en-têtes d'authentification vers l'amont",
|
||||||
|
"aiProviderRoutingMode": "Mode de routage",
|
||||||
|
"aiProviderRoutingModeDescription": "Envoyez le trafic vers une URL amont ou vers des cibles HTTP sur vos sites",
|
||||||
|
"aiProviderRoutingModeUrl": "URL amont",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Appelez une URL de base d'API publique ou privée",
|
||||||
|
"aiProviderRoutingModeTarget": "Cibles du site",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Route à travers les cibles sur vos sites",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Après avoir créé ce fournisseur, configurez les cibles du site dans l'onglet Paramètres du réseau.",
|
||||||
|
"aiProviderTargetNoOne": "Ce fournisseur n'a aucune cible. Ajoutez une cible pour acheminer les requêtes via vos sites.",
|
||||||
|
"aiProviderSkipTlsVerification": "Ignorer la vérification TLS",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Désactiver la vérification du certificat TLS pour la connexion amont",
|
||||||
|
"aiProviderBudget": "Budget",
|
||||||
|
"aiProviderBudgetDescription": "Budget de dépenses ou de jetons optionnel pour ce fournisseur",
|
||||||
|
"aiProviderBudgetAmount": "Montant du budget",
|
||||||
|
"aiProviderBudgetUnit": "Unité de budget",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Jetons",
|
||||||
|
"aiProviderEnabled": "Activé",
|
||||||
|
"aiProviderEnabledDescription": "Désactivez complètement ce fournisseur sur toutes les ressources",
|
||||||
|
"aiProviderErrorCreate": "Échec de la création du fournisseur AI",
|
||||||
|
"aiProviderErrorUpdate": "Échec de la mise à jour du fournisseur AI",
|
||||||
|
"aiProviderErrorDelete": "Échec de la suppression du fournisseur AI",
|
||||||
|
"aiProviderErrorLoad": "Échec du chargement du fournisseur AI",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Entrez une URL amont valide",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Une URL amont est requise pour ce fournisseur",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Le type d'authentification est requis",
|
||||||
|
"aiProviderErrorApiKeyRequired": "La clé API est requise",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Le routage des cibles du site est uniquement disponible pour les fournisseurs personnalisés",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Sélectionnez au moins une capacité API",
|
||||||
|
"aiProviderCapabilities": "Capacités API",
|
||||||
|
"aiProviderCapabilitiesDescription": "Sélectionnez les formats d'API que ce fournisseur peut gérer. Les fournisseurs connus commencent avec des réglages recommandés.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Sélectionnez quels formats d'API ce fournisseur personnalisé peut gérer",
|
||||||
|
"aiProviderCapabilitiesSelect": "Sélectionner les capacités",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Aucune capacité trouvée",
|
||||||
|
"aiProviderCapabilitiesSearch": "Rechercher des capacités...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Completions de Chat",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Prend en charge /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "Réponses OpenAI",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Prend en charge /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Messages Anthropiques",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Prend en charge /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Générer du Contenu Gemini",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Prend en charge l'API directe de Gemini",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Invocation du Modèle Bedrock",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Prend en charge Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Générer du Contenu Vertex",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Prend en charge le format Gemini de Vertex AI",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Prédiction Brute Vertex",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Prend en charge la prédiction brute Vertex AI pour les modèles anthropiques",
|
||||||
|
"aiCapabilityBedrockConverse": "Converse Bedrock",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Prend en charge l'API Converse d'Amazon Bedrock",
|
||||||
|
"aiProviderCreated": "Fournisseur d'IA créé",
|
||||||
|
"aiProviderUpdated": "Fournisseur d'IA mis à jour",
|
||||||
|
"aiProviderDeleted": "Fournisseur d'IA supprimé",
|
||||||
|
"aiProviderDelete": "Supprimer le fournisseur",
|
||||||
|
"aiProviderDeleteConfirm": "Supprimer le fournisseur",
|
||||||
|
"aiProviderQuestionRemove": "Êtes-vous sûr de vouloir supprimer ce fournisseur d'IA ?",
|
||||||
|
"aiProviderMessageRemove": "Cela supprimera définitivement le fournisseur ainsi que ses modèles et cibles. Cela ne peut pas être annulé.",
|
||||||
|
"aiProviderErrorNoUpdate": "Le fournisseur d'IA n'est pas disponible pour la mise à jour",
|
||||||
|
"aiProviderModels": "Modèles",
|
||||||
|
"aiProviderModelsDescription": "Définissez les listes d'autorisation et de blocage pour ce fournisseur. Les demandes doivent correspondre à une entrée autorisée et ne doivent pas correspondre à une entrée bloquée.",
|
||||||
|
"aiProviderCreateModelsDescription": "Choisissez quels modèles ce fournisseur peut desservir. Une liste d'autorisation vide bloque tout le trafic. Vous pouvez ajouter ou modifier des modèles plus tard.",
|
||||||
|
"aiProviderModelsPlaceholder": "Recherchez des modèles ou tapez une clé personnalisée",
|
||||||
|
"aiProviderModelsAllow": "Liste d'autorisation",
|
||||||
|
"aiProviderModelsAllowDescription": "Modèles qui peuvent être utilisés via ce fournisseur. Vide signifie tout bloquer.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Entrez la clé du modèle",
|
||||||
|
"aiProviderModelsAllowEmpty": "Pas de modèles ajoutés pour le moment.",
|
||||||
|
"aiProviderModelsBlock": "Liste de blocage",
|
||||||
|
"aiProviderModelsBlockDescription": "Modèles à refuser même s'ils correspondent à une entrée autorisée.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Entrez la clé du modèle",
|
||||||
|
"aiProviderModelsBlockEmpty": "Pas de modèles ajoutés pour le moment.",
|
||||||
|
"aiProviderModelsAdd": "Ajouter des modèles",
|
||||||
|
"aiProviderModelsClearAll": "Effacer tout",
|
||||||
|
"aiProviderModelsAddCustom": "Ajouter \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Appuyez sur Entrée pour ajouter cette clé de modèle personnalisée.",
|
||||||
|
"aiProviderModelsAddBulk": "Ajouter {count} clés personnalisées",
|
||||||
|
"aiProviderModelsAddBulkHint": "Appuyez sur Entrée pour ajouter {count} clés personnalisées.",
|
||||||
|
"aiProviderModelsAddOne": "Ajouter {key} maintenant",
|
||||||
|
"aiProviderModelsAddSelected": "Ajouter la sélection",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} sélectionné(s)",
|
||||||
|
"aiProviderModelsSelectAll": "Tout sélectionner",
|
||||||
|
"aiProviderModelsClearSelected": "Effacer",
|
||||||
|
"aiProviderModelsBulkHint": "Sélectionnez des modèles connus ou tapez une clé personnalisée.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Aucun modèle de catalogue correspondant.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Modèles connus",
|
||||||
|
"aiProviderModelsAllLabel": "Tous les modèles",
|
||||||
|
"aiProviderModelsAllPatternHint": "Joker : *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Autoriser tous les modèles",
|
||||||
|
"aiProviderModelsAddAllBlock": "Bloquer tous les modèles",
|
||||||
|
"aiProviderModelsAddAllDescription": "Utilise le modèle * joker pour que chaque clé de modèle corresponde.",
|
||||||
|
"aiProviderModelsViewMore": "Voir plus ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Voir moins",
|
||||||
|
"aiProviderModelsRemove": "Supprimer le modèle",
|
||||||
|
"aiProviderModelsEditHint": "Cliquez pour modifier les paramètres du modèle",
|
||||||
|
"aiProviderModelsSourceCatalog": "Modèle de catalogue connu",
|
||||||
|
"aiProviderModelsSourceCustom": "Clé de modèle personnalisée",
|
||||||
|
"aiProviderModelsSourcePattern": "Modèle de correspondance avec générique",
|
||||||
|
"aiProviderModelsSourceAll": "Correspond à chaque clé de modèle",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Budget configuré",
|
||||||
|
"aiProviderModelsEditTitle": "Modifier le modèle",
|
||||||
|
"aiProviderModelsEditDescription": "Mettez à jour la clé du modèle ou configurez son budget.",
|
||||||
|
"aiProviderModelsBudgetTab": "Budget",
|
||||||
|
"aiProviderModelsKeyLabel": "Clé du modèle",
|
||||||
|
"aiProviderModelsKeyRequired": "Entrez une clé de modèle",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Cette clé de modèle est déjà sur une liste",
|
||||||
|
"aiProviderModelsOverlapError": "Ces modèles ne peuvent être sur les deux listes : {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modèles mis à jour",
|
||||||
|
"aiProviderModelsErrorUpdate": "Échec de la mise à jour des modèles",
|
||||||
|
"aiResourceProviders": "Fournisseurs",
|
||||||
|
"aiResourceProvidersDescription": "Choisissez quels fournisseurs d'IA ce portail AI peut utiliser",
|
||||||
|
"aiResourceProvidersHelp": "Joignez des fournisseurs et choisissez d'hériter (utiliser les listes de chaque fournisseur) ou de sélectionner (choisir une liste autorisée pour cette ressource). Les modèles conflictuels des fournisseurs attachés ne sont pas autorisés.",
|
||||||
|
"aiResourceProvidersSelect": "Sélectionnez les fournisseurs",
|
||||||
|
"aiResourceProvidersEmpty": "Aucun fournisseur d'IA trouvé",
|
||||||
|
"aiResourceProvidersNoneAttached": "Aucun fournisseur n'est attaché pour le moment.",
|
||||||
|
"aiResourceProvidersAdd": "Ajouter un fournisseur",
|
||||||
|
"aiResourceProvidersRemove": "Supprimer le fournisseur",
|
||||||
|
"aiResourceProviderToggleEnabled": "Activer ou désactiver ce fournisseur sur la ressource",
|
||||||
|
"aiResourceProviderDisabled": "Désactivé",
|
||||||
|
"aiResourceProvidersUpdated": "Fournisseurs mis à jour",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Échec de la mise à jour des fournisseurs",
|
||||||
|
"aiResourceProviderEditDescription": "Choisissez comment les modèles de ce fournisseur sont exposés sur cette ressource.",
|
||||||
|
"viewProviderSettings": "Voir les paramètres du fournisseur",
|
||||||
|
"aiResourceProviderMode": "Mode d'accès",
|
||||||
|
"aiResourceProviderModeInherit": "Hériter",
|
||||||
|
"aiResourceProviderModeSelect": "Sélectionner",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Sélectionner · {count} modèles",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Utilisez les listes d'autorisation et de blocage de ce fournisseur comme configuré sur le fournisseur.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Choisissez un sous-ensemble des modèles de liste autorisée de ce fournisseur pour cette ressource.",
|
||||||
|
"aiResourceProviderAllowModels": "Liste autorisée",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Sélectionnez des modèles",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Rechercher des modèles...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Aucun modèle trouvé",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Seuls les modèles de la liste autorisée de ce fournisseur peuvent être sélectionnés.",
|
||||||
|
"aiResourceAliasRequired": "L'alias est requis pour les portails AI",
|
||||||
|
"aiResourceDomainConfiguration": "Configuration du Domaine",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Choisissez le domaine que les clients utiliseront pour atteindre ce portail AI.",
|
||||||
|
"aiUsageAnalyticsTitle": "Analytique d'Utilisation d'IA",
|
||||||
|
"aiUsageAnalyticsDescription": "Analysez le coût, l'utilisation des jetons et l'activité du portail AI avec les fournisseurs, les ressources, les rôles et les utilisateurs",
|
||||||
|
"aiUsageTabOverview": "Vue d'ensemble",
|
||||||
|
"aiUsageTabProviders": "Fournisseurs",
|
||||||
|
"aiUsageTabResources": "Ressources",
|
||||||
|
"aiUsageFilterProvider": "Fournisseur",
|
||||||
|
"aiUsageFilterModel": "Modèle",
|
||||||
|
"aiUsageFilterResource": "Ressource",
|
||||||
|
"aiUsageFilterRole": "Rôle",
|
||||||
|
"aiUsageFilterUser": "Utilisateur",
|
||||||
|
"aiUsageFilterAllProviders": "Tous les Fournisseurs",
|
||||||
|
"aiUsageFilterAllModels": "Tous les Modèles",
|
||||||
|
"aiUsageFilterAllResources": "Toutes les Ressources",
|
||||||
|
"aiUsageFilterAllRoles": "Tous les Rôles",
|
||||||
|
"aiUsageFilterAllUsers": "Tous les Utilisateurs",
|
||||||
|
"aiUsageFilterSearch": "Rechercher...",
|
||||||
|
"aiUsageFilterNotFound": "Aucune option trouvée",
|
||||||
|
"aiUsageResetFilters": "Réinitialiser les filtres",
|
||||||
|
"aiUsageRefresh": "Actualiser",
|
||||||
|
"aiUsageTokenTypePrompt": "Invite",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Lecture du cache",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Écriture du cache",
|
||||||
|
"aiUsageTokenTypeCompletion": "Achèvement",
|
||||||
|
"aiUsageTokenTypeReasoning": "Raisonnement",
|
||||||
|
"aiUsageRequests": "Requêtes",
|
||||||
|
"aiUsageCost": "Coût",
|
||||||
|
"aiUsageTokens": "Jetons",
|
||||||
|
"aiUsageOther": "Autre",
|
||||||
|
"aiUsageTotalRequests": "Total des Requêtes",
|
||||||
|
"aiUsageTotalTokens": "Total des Jetons",
|
||||||
|
"aiUsageTotalCost": "Coût Total",
|
||||||
|
"aiUsageEstimated": "Estimé",
|
||||||
|
"aiUsageRequestVolume": "Volume de Requêtes",
|
||||||
|
"aiUsageTokenUsage": "Usage de Jeton",
|
||||||
|
"aiUsageModelCost": "Coût de Modèle",
|
||||||
|
"aiUsageModelTokens": "Jetons de Modèle",
|
||||||
|
"aiUsageTopModels": "Meilleurs Modèles",
|
||||||
|
"aiUsageTopProviders": "Meilleurs Fournisseurs",
|
||||||
|
"aiUsageProviderCost": "Coût du Fournisseur",
|
||||||
|
"aiUsageProviderTokenUsage": "Usage de Jeton du Fournisseur",
|
||||||
|
"aiUsageTopResources": "Meilleures Ressources",
|
||||||
|
"aiUsageResourceCost": "Coût des Ressources",
|
||||||
|
"aiUsageResourceTokenUsage": "Usage de Jeton des Ressources",
|
||||||
|
"aiUsageResourceTypePublic": "Ressource Publique",
|
||||||
|
"aiUsageResourceTypeSite": "Ressource Privée",
|
||||||
|
"aiUsageNoResource": "Pas de ressource",
|
||||||
|
"aiUsageRolesTab": "Rôles",
|
||||||
|
"aiUsageUsersTab": "Utilisateurs",
|
||||||
|
"aiUsageTopRoles": "Meilleurs Rôles",
|
||||||
|
"aiUsageRoleCost": "Coût du Rôle",
|
||||||
|
"aiUsageRoleTokenUsage": "Usage de Jeton du Rôle",
|
||||||
|
"aiUsageTopUsers": "Meilleurs Utilisateurs",
|
||||||
|
"aiUsageUserCost": "Coût de l'Utilisateur",
|
||||||
|
"aiUsageUserTokenUsage": "Usage de Jeton de l'Utilisateur",
|
||||||
|
"aiUsageUnknownUser": "Utilisateur Inconnu",
|
||||||
|
"aiUsageVirtualApiKeysTab": "API Clés Virtuelles",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Clé API Virtuelle",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Toutes les Clés API Virtuelles",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Meilleures Clés API Virtuelles",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Coût de la Clé API Virtuelle",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Usage de Jeton de la Clé API Virtuelle",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Pas de clé API virtuelle",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Clé sans nom",
|
||||||
|
"aiUsageLoading": "Chargement...",
|
||||||
|
"aiUsageNoData": "Pas de données",
|
||||||
|
"resourceBudgetSettings": "Budget",
|
||||||
|
"resourceBudgetSettingsDescription": "Configurez comment ce portail AI limite l'utilisation en fonction des dépenses ou des limites de jetons",
|
||||||
"sidebarApiKeys": "Clés API",
|
"sidebarApiKeys": "Clés API",
|
||||||
"sidebarProvisioning": "Mise en place",
|
"sidebarProvisioning": "Mise en place",
|
||||||
"sidebarSettings": "Réglages",
|
"sidebarSettings": "Réglages",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Sous-réseau",
|
"subnetPlaceholder": "Sous-réseau",
|
||||||
"addressDescription": "L'adresse interne du client. Doit être dans le sous-réseau de l'organisation.",
|
"addressDescription": "L'adresse interne du client. Doit être dans le sous-réseau de l'organisation.",
|
||||||
"selectSites": "Sélectionner des sites",
|
"selectSites": "Sélectionner des sites",
|
||||||
|
"selectResources": "Sélectionner des ressources",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# ressource} other {# ressources}}",
|
||||||
"selectLabels": "Sélectionner des étiquettes",
|
"selectLabels": "Sélectionner des étiquettes",
|
||||||
"sitesDescription": "Le client aura une connectivité vers les sites sélectionnés",
|
"sitesDescription": "Le client aura une connectivité vers les sites sélectionnés",
|
||||||
"clientInstallOlm": "Installer Olm",
|
"clientInstallOlm": "Installer Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Le temps est exprimé en secondes",
|
"timeIsInSeconds": "Le temps est exprimé en secondes",
|
||||||
"requireDeviceApproval": "Exiger les autorisations de l'appareil",
|
"requireDeviceApproval": "Exiger les autorisations de l'appareil",
|
||||||
"requireDeviceApprovalDescription": "Les utilisateurs ayant ce rôle ont besoin de nouveaux périphériques approuvés par un administrateur avant de pouvoir se connecter et accéder aux ressources.",
|
"requireDeviceApprovalDescription": "Les utilisateurs ayant ce rôle ont besoin de nouveaux périphériques approuvés par un administrateur avant de pouvoir se connecter et accéder aux ressources.",
|
||||||
"sshSettings": "Paramètres SSH",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "Portail AI",
|
||||||
"sshAccess": "Accès SSH",
|
"sshAccess": "Accès SSH",
|
||||||
"rdpSettings": "Paramètres RDP",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "Paramètres VNC",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "Serveur SSH",
|
"sshServer": "Serveur SSH",
|
||||||
"rdpServer": "Serveur RDP",
|
"rdpServer": "Serveur RDP",
|
||||||
"vncServer": "Serveur VNC",
|
"vncServer": "Serveur VNC",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "Portail AI",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Méthode HTTP",
|
"editInternalResourceDialogScheme": "Méthode HTTP",
|
||||||
"editInternalResourceDialogEnableSsl": "Activer TLS",
|
"editInternalResourceDialogEnableSsl": "Activer TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Autoriser le ping ICMP",
|
"privateResourceAllowIcmpPing": "Autoriser le ping ICMP",
|
||||||
"privateResourceNetworkAccess": "Accès au réseau",
|
"privateResourceNetworkAccess": "Accès au réseau",
|
||||||
"privateResourceNetworkAccessDescription": "Contrôler l'accès aux ports TCP/UDP et si le ping ICMP est autorisé pour cette ressource.",
|
"privateResourceNetworkAccessDescription": "Contrôler l'accès aux ports TCP/UDP et si le ping ICMP est autorisé pour cette ressource.",
|
||||||
"hostSettings": "Paramètres de l'hôte",
|
"hostSettings": "Hôte",
|
||||||
"cidrSettings": "Paramètres CIDR",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Propriétés de la ressource",
|
"createInternalResourceDialogResourceProperties": "Propriétés de la ressource",
|
||||||
"createInternalResourceDialogName": "Nom",
|
"createInternalResourceDialogName": "Nom",
|
||||||
"createInternalResourceDialogSite": "Site",
|
"createInternalResourceDialogSite": "Site",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "Portail AI",
|
||||||
"scheme": "Méthode HTTP",
|
"scheme": "Méthode HTTP",
|
||||||
"createInternalResourceDialogScheme": "Méthode HTTP",
|
"createInternalResourceDialogScheme": "Méthode HTTP",
|
||||||
"createInternalResourceDialogEnableSsl": "Activer TLS",
|
"createInternalResourceDialogEnableSsl": "Activer TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Mot de passe valide",
|
"validPassword": "Mot de passe valide",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Clé API Virtuelle Valide",
|
||||||
"view": "Afficher",
|
"view": "Afficher",
|
||||||
"configManaged": "Configuration gérée",
|
"configManaged": "Configuration gérée",
|
||||||
"connectedClient": "Client connecté",
|
"connectedClient": "Client connecté",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Journaliser la rétention",
|
"logRetention": "Journaliser la rétention",
|
||||||
"logRetentionDescription": "Gérer la durée de conservation des différents types de logs pour cette organisation ou les désactiver",
|
"logRetentionDescription": "Gérer la durée de conservation des différents types de logs pour cette organisation ou les désactiver",
|
||||||
"requestLogsDescription": "Voir les journaux détaillés des requêtes pour les ressources de cette organisation",
|
"requestLogsDescription": "Voir les journaux détaillés des requêtes pour les ressources de cette organisation",
|
||||||
|
"aiSessionLogs": "Journaux de Session du Portail AI",
|
||||||
|
"aiSessionLogsDescription": "Voir les transcriptions de l'invite et de la réponse pour les requêtes de portail AI dans cette organisation",
|
||||||
|
"sidebarLogsAi": "Journaux de Session",
|
||||||
|
"commandLogsAi": "Journaux de Session",
|
||||||
|
"sidebarLogsAiUsage": "Analytique d'Utilisation",
|
||||||
|
"commandLogsAiUsage": "Analytique d'Utilisation",
|
||||||
|
"provider": "Fournisseur",
|
||||||
|
"capability": "Capacité",
|
||||||
|
"model": "Modèle",
|
||||||
|
"virtualApiKey": "Clé API Virtuelle",
|
||||||
|
"noVirtualApiKey": "Pas de clé API virtuelle",
|
||||||
|
"stream": "Flux",
|
||||||
|
"streaming": "En Flux",
|
||||||
|
"nonStreaming": "Non-Flux",
|
||||||
|
"statusCode": "Code d'État",
|
||||||
|
"aiSessionId": "ID de Session",
|
||||||
|
"aiSessionRequest": "Requête",
|
||||||
|
"aiSessionResponse": "Réponse",
|
||||||
|
"aiSessionNoData": "Aucune donnée capturée",
|
||||||
|
"aiSessionCouldNotParse": "(brut, n'a pas pu analyser la transcription)",
|
||||||
|
"aiSessionLogTruncated": "Cette session a été tronquée avant le stockage et peut être incomplète.",
|
||||||
|
"aiSessionViewRaw": "Afficher le JSON Brut",
|
||||||
|
"aiSessionViewChat": "Afficher la Discussion",
|
||||||
|
"cost": "Coût",
|
||||||
|
"estimated": "Estimé",
|
||||||
|
"tokenUsage": "Usage de Jeton",
|
||||||
|
"promptTokens": "Jetons Prompt",
|
||||||
|
"cacheReadTokens": "Jetons Lecture Cache",
|
||||||
|
"cacheWriteTokens": "Jetons Écriture Cache",
|
||||||
|
"completionTokens": "Jetons Completion",
|
||||||
|
"reasoningTokens": "Jetons Raisonnement",
|
||||||
|
"totalTokens": "Total des Jetons",
|
||||||
"requestAnalyticsDescription": "Voir les analyses détaillées des demandes pour les ressources de cette organisation",
|
"requestAnalyticsDescription": "Voir les analyses détaillées des demandes pour les ressources de cette organisation",
|
||||||
"logRetentionRequestLabel": "Rétention des Journaux de Requêtes HTTP",
|
"logRetentionRequestLabel": "Rétention des Journaux de Requêtes HTTP",
|
||||||
"logRetentionRequestDescription": "Durée de conservation des journaux de requêtes",
|
"logRetentionRequestDescription": "Durée de conservation des journaux de requêtes",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Durée de conservation du journal des actions",
|
"logRetentionActionDescription": "Durée de conservation du journal des actions",
|
||||||
"logRetentionConnectionLabel": "Rétention du journal de connexion",
|
"logRetentionConnectionLabel": "Rétention du journal de connexion",
|
||||||
"logRetentionConnectionDescription": "Durée de conservation des logs de connexion",
|
"logRetentionConnectionDescription": "Durée de conservation des logs de connexion",
|
||||||
|
"logRetentionAISessionsLabel": "Rétention des Journaux des Sessions du Portail AI",
|
||||||
|
"logRetentionAISessionsDescription": "Combien de temps conserver les journaux des sessions d'invite/réponse du portail AI",
|
||||||
"logRetentionDisabled": "Désactivé",
|
"logRetentionDisabled": "Désactivé",
|
||||||
"logRetention3Days": "3 jours",
|
"logRetention3Days": "3 jours",
|
||||||
"logRetention7Days": "7 jours",
|
"logRetention7Days": "7 jours",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Adresse source",
|
"sourceAddress": "Adresse source",
|
||||||
"destinationAddress": "Adresse de destination",
|
"destinationAddress": "Adresse de destination",
|
||||||
"duration": "Durée",
|
"duration": "Durée",
|
||||||
"licenseRequiredToUse": "Une <enterpriseLicenseLink>licence Enterprise Edition</enterpriseLicenseLink> ou <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> est requise pour utiliser cette fonctionnalité. <bookADemoLink>Réservez une démo gratuite ou un essai POC pour en savoir plus.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Licence Enterprise Edition</enterpriseEditionLink> requise. <bookADemoLink>Réserver une démo ou un essai</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "La version <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> est requise pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Réservez une démo gratuite ou un essai POC pour en savoir plus.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Licence Enterprise Edition</enterpriseEditionLink> requise. <bookADemoLink>Réserver une démo ou un essai</bookADemoLink>",
|
||||||
"certResolver": "Résolveur de certificat",
|
"certResolver": "Résolveur de certificat",
|
||||||
"certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.",
|
"certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.",
|
||||||
"selectCertResolver": "Sélectionnez le résolveur de certificat",
|
"selectCertResolver": "Sélectionnez le résolveur de certificat",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "La sélection de plusieurs sites permet un routage résilient et un basculement pour une haute disponibilité.",
|
"internalResourceFormMultiSiteRoutingHelp": "La sélection de plusieurs sites permet un routage résilient et un basculement pour une haute disponibilité.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "En savoir plus",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "En savoir plus",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Restreindre l'accès à des ports TCP/UDP spécifiques ou autoriser/bloquer tous les ports.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Restreindre l'accès à des ports TCP/UDP spécifiques ou autoriser/bloquer tous les ports.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "Configuration HTTP",
|
"createInternalResourceDialogHttpConfiguration": "Configuration du Domaine",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Choisissez le domaine que les clients utiliseront pour atteindre cette ressource via HTTP ou HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Choisissez le domaine que les clients utiliseront pour atteindre cette ressource via HTTP ou HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "Configuration HTTP",
|
"editInternalResourceDialogHttpConfiguration": "Configuration du Domaine",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Choisissez le domaine que les clients utiliseront pour atteindre cette ressource via HTTP ou HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Choisissez le domaine que les clients utiliseront pour atteindre cette ressource via HTTP ou HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Non étiqueté",
|
"resourceLauncherUnlabeled": "Non étiqueté",
|
||||||
|
"resourceLauncherAiGateway": "Portail AI",
|
||||||
"resourceLauncherNoSite": "Aucun nœud",
|
"resourceLauncherNoSite": "Aucun nœud",
|
||||||
|
"resourceLauncherAvailableModels": "Modèles Disponibles",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modèles que vous pouvez utiliser avec ce portail AI.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Aucun modèle n'est disponible pour cette ressource.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Impossible de charger les modèles disponibles.",
|
||||||
|
"resourceLauncherApiKeys": "Clés API",
|
||||||
|
"resourceLauncherApiKeysDescription": "Utilisez votre clé d'identité ou une clé attribuée pour vous authentifier avec cette ressource.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Clé d'identité",
|
||||||
|
"resourceLauncherApiKeysManual": "Clés Attribuées",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Pas de clés API disponibles pour cette ressource.",
|
||||||
|
"resourceLauncherApiKeysError": "Impossible de charger les clés API.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Aucune ressource dans ce groupe",
|
"resourceLauncherNoResourcesInGroup": "Aucune ressource dans ce groupe",
|
||||||
"resourceLauncherEmptyStateTitle": "Aucune ressource disponible",
|
"resourceLauncherEmptyStateTitle": "Aucune ressource disponible",
|
||||||
"resourceLauncherEmptyStateDescription": "Vous n'avez pas encore accès à des ressources. Contactez votre administrateur pour demander l'accès.",
|
"resourceLauncherEmptyStateDescription": "Vous n'avez pas encore accès à des ressources. Contactez votre administrateur pour demander l'accès.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Impossible de supprimer la vue de lancement. Veuillez réessayer.",
|
"resourceLauncherViewDeleteFailedDescription": "Impossible de supprimer la vue de lancement. Veuillez réessayer.",
|
||||||
"memberPortalPrevious": "Précédent",
|
"memberPortalPrevious": "Précédent",
|
||||||
"memberPortalNext": "Suivant",
|
"memberPortalNext": "Suivant",
|
||||||
"httpSettings": "Paramètres HTTP",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "Paramètres TCP",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "Paramètres UDP",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Établissement d'une connexion sécurisée…",
|
"sshConnectingDescription": "Établissement d'une connexion sécurisée…",
|
||||||
"sshConnecting": "Connexion…",
|
"sshConnecting": "Connexion…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Assicurati di aver effettuato l'accesso come utente corretto.",
|
"inviteLoginUser": "Assicurati di aver effettuato l'accesso come utente corretto.",
|
||||||
"inviteErrorNoUser": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia per un utente che esiste.",
|
"inviteErrorNoUser": "Siamo spiacenti, ma sembra che l'invito che stai cercando di accedere non sia per un utente che esiste.",
|
||||||
"inviteCreateUser": "Si prega di creare un account prima.",
|
"inviteCreateUser": "Si prega di creare un account prima.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Gli inviti possono essere accettati solo da account interni. Esci e accedi con la tua password per questa email.",
|
||||||
|
"inviteLoginInternalOnly": "Gli inviti richiedono un account interno con una password. Crea un account o accedi con la tua password.",
|
||||||
"goHome": "Vai alla Home",
|
"goHome": "Vai alla Home",
|
||||||
"inviteLogInOtherUser": "Accedi come utente diverso",
|
"inviteLogInOtherUser": "Accedi come utente diverso",
|
||||||
"createAnAccount": "Crea un account",
|
"createAnAccount": "Crea un account",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Nessun paese trovato.",
|
"noCountryFound": "Nessun paese trovato.",
|
||||||
"siteSelectionDescription": "Questo sito fornirà connettività all'oggetto target.",
|
"siteSelectionDescription": "Questo sito fornirà connettività all'oggetto target.",
|
||||||
"resourceType": "Tipo Di Risorsa",
|
"resourceType": "Tipo Di Risorsa",
|
||||||
"resourceTypeDescription": "Questo controlla il protocollo delle risorse e come verrà reso nel browser. Questo non può essere modificato in seguito.",
|
"resourceTypeDescription": "Questo controlla il protocollo della risorsa e non può essere cambiato in seguito.",
|
||||||
|
"resourceTypeSearch": "Cerca tipi di risorse...",
|
||||||
|
"resourceTypeNotFound": "Nessun tipo di risorsa trovato",
|
||||||
|
"resourceTypeHttpDescription": "Proxy del traffico web su HTTPS usando un nome di dominio",
|
||||||
|
"resourceTypeInferenceDescription": "Instrada le richieste AI tramite i provider allegati",
|
||||||
|
"resourceTypeSshDescription": "Accedi a un server SSH dal browser",
|
||||||
|
"resourceTypeRdpDescription": "Accedi a un desktop remoto dal browser",
|
||||||
|
"resourceTypeVncDescription": "Accedi a un desktop VNC dal browser",
|
||||||
|
"resourceTypeTcpDescription": "Proxy del traffico TCP grezzo utilizzando un numero di porta",
|
||||||
|
"resourceTypeUdpDescription": "Proxy del traffico UDP grezzo utilizzando un numero di porta",
|
||||||
|
"privateResourceTypeDescription": "Questo controlla come i client raggiungono la risorsa. Non può essere modificato in seguito.",
|
||||||
|
"privateResourceTypeHostDescription": "Espone un singolo host sulla rete del sito ai client connessi",
|
||||||
|
"privateResourceTypeCidrDescription": "Espone un intervallo CIDR sulla rete del sito ai client connessi",
|
||||||
|
"privateResourceTypeHttpDescription": "Accedi a un servizio HTTP o HTTPS tramite un dominio",
|
||||||
|
"privateResourceTypeSshDescription": "Accedi a un server SSH dai client connessi",
|
||||||
"resourceDomainDescription": "La risorsa sarà servita su questo dominio completamente qualificato.",
|
"resourceDomainDescription": "La risorsa sarà servita su questo dominio completamente qualificato.",
|
||||||
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
"resourceHTTPSSettings": "Impostazioni HTTPS",
|
||||||
"resourceHTTPSSettingsDescription": "Configura come sarà possibile accedere alla risorsa su HTTPS",
|
"resourceHTTPSSettingsDescription": "Configura come sarà possibile accedere alla risorsa su HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Salva Impostazioni",
|
"authMethodsSave": "Salva Impostazioni",
|
||||||
"policyAuthStackTitle": "Autenticazione",
|
"policyAuthStackTitle": "Autenticazione",
|
||||||
"policyAuthStackDescription": "Controlla quali metodi di autenticazione sono richiesti per accedere a questa risorsa",
|
"policyAuthStackDescription": "Controlla quali metodi di autenticazione sono richiesti per accedere a questa risorsa",
|
||||||
|
"policyAuthInferenceStackDescription": "Scegli quali utenti e ruoli possono autenticarsi a questo gateway AI",
|
||||||
"policyAuthOrLogicTitle": "Più metodi di autenticazione attivi",
|
"policyAuthOrLogicTitle": "Più metodi di autenticazione attivi",
|
||||||
"policyAuthOrLogicBanner": "I visitatori possono autenticarsi utilizzando uno qualsiasi dei metodi attivi sottostanti. Non è necessario completarli tutti.",
|
"policyAuthOrLogicBanner": "I visitatori possono autenticarsi utilizzando uno qualsiasi dei metodi attivi sottostanti. Non è necessario completarli tutti.",
|
||||||
"policyAuthMethodActive": "Attivo",
|
"policyAuthMethodActive": "Attivo",
|
||||||
"policyAuthMethodOff": "Disattivo",
|
"policyAuthMethodOff": "Disattivo",
|
||||||
"policyAuthSsoTitle": "SSO della Piattaforma",
|
"policyAuthSsoTitle": "SSO della Piattaforma",
|
||||||
"policyAuthSsoDescription": "Richiedi l'accesso tramite il provider di identità della tua organizzazione",
|
"policyAuthSsoDescription": "Richiedi l'accesso tramite il provider di identità della tua organizzazione",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "URL di accesso",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Ogni utente ha già una chiave API di identità, quindi devi solo creare chiavi API virtuali per i client non utente o per l'accesso condiviso. Gli utenti possono recuperare la loro chiave accedendo con il loro provider di identità all'URL di questa risorsa, dove verrà mostrata dopo il login.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Ogni utente ha già una chiave API di identità, quindi devi solo creare chiavi API virtuali per i client non utente o per l'accesso condiviso. Gli utenti recuperano la loro chiave qui dopo aver effettuato l'accesso con il loro provider di identità.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} utenti, {roles} ruoli",
|
"policyAuthSsoSummary": "{idp} · {users} utenti, {roles} ruoli",
|
||||||
"policyAuthSsoDefaultIdp": "Provider predefinito",
|
"policyAuthSsoDefaultIdp": "Provider predefinito",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Aggiungi Provider di Identità Predefinito",
|
"policyAuthAddDefaultIdentityProvider": "Aggiungi Provider di Identità Predefinito",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Nuovo ruolo richiesto",
|
"accessRoleErrorNewRequired": "Nuovo ruolo richiesto",
|
||||||
"accessRoleErrorRemove": "Impossibile rimuovere il ruolo",
|
"accessRoleErrorRemove": "Impossibile rimuovere il ruolo",
|
||||||
"accessRoleErrorRemoveDescription": "Si è verificato un errore durante la rimozione del ruolo.",
|
"accessRoleErrorRemoveDescription": "Si è verificato un errore durante la rimozione del ruolo.",
|
||||||
|
"accessRoleInferenceBudget": "Budget AI",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Configura come i membri di questo ruolo limitano l'uso dell'AI in base alla spesa o ai limiti di token",
|
||||||
"accessRoleName": "Nome Del Ruolo",
|
"accessRoleName": "Nome Del Ruolo",
|
||||||
"accessRoleQuestionRemove": "Stai per eliminare il ruolo `{name}`. Non puoi annullare questa azione.",
|
"accessRoleQuestionRemove": "Stai per eliminare il ruolo `{name}`. Non puoi annullare questa azione.",
|
||||||
"accessRoleRemove": "Rimuovi Ruolo",
|
"accessRoleRemove": "Rimuovi Ruolo",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Elimina Sito",
|
"actionDeleteSite": "Elimina Sito",
|
||||||
"actionGetSite": "Ottieni Sito",
|
"actionGetSite": "Ottieni Sito",
|
||||||
"actionListSites": "Elenca Siti",
|
"actionListSites": "Elenca Siti",
|
||||||
|
"actionCreateAiProvider": "Crea Provider AI",
|
||||||
|
"actionDeleteAiProvider": "Elimina Provider AI",
|
||||||
|
"actionGetAiProvider": "Ottieni Provider AI",
|
||||||
|
"actionListAiProviders": "Elenca Provider AI",
|
||||||
|
"actionUpdateAiProvider": "Aggiorna Provider AI",
|
||||||
|
"actionCreateAiModel": "Crea Modello AI",
|
||||||
|
"actionDeleteAiModel": "Elimina Modello AI",
|
||||||
|
"actionGetAiModel": "Ottieni Modello AI",
|
||||||
|
"actionListAiModels": "Elenca Modelli AI",
|
||||||
|
"actionUpdateAiModel": "Aggiorna Modello AI",
|
||||||
|
"actionCreateAiBudget": "Crea Budget AI",
|
||||||
|
"actionDeleteAiBudget": "Elimina Budget AI",
|
||||||
|
"actionGetAiBudget": "Ottieni Budget AI",
|
||||||
|
"actionListAiBudgets": "Elenca Budget AI",
|
||||||
|
"actionUpdateAiBudget": "Aggiorna Budget AI",
|
||||||
|
"actionListResourceAiModels": "Elenca Modelli AI Risorsa",
|
||||||
|
"actionSetResourceAiModels": "Imposta Modelli AI Risorsa",
|
||||||
|
"actionCreateVirtualApiKey": "Crea Chiave API Virtuale",
|
||||||
|
"actionDeleteVirtualApiKey": "Elimina Chiave API Virtuale",
|
||||||
|
"actionGetVirtualApiKey": "Ottieni Chiave API Virtuale",
|
||||||
|
"actionListVirtualApiKeys": "Elenca Chiavi API Virtuali",
|
||||||
|
"actionUpdateVirtualApiKey": "Aggiorna Chiave API Virtuale",
|
||||||
"actionApplyBlueprint": "Applica Progetto",
|
"actionApplyBlueprint": "Applica Progetto",
|
||||||
"actionListBlueprints": "Elenco Blueprints",
|
"actionListBlueprints": "Elenco Blueprints",
|
||||||
"actionGetBlueprint": "Ottieni Blueprint",
|
"actionGetBlueprint": "Ottieni Blueprint",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Imposta Codice PIN Risorsa",
|
"actionSetResourcePincode": "Imposta Codice PIN Risorsa",
|
||||||
"actionSetResourceEmailWhitelist": "Imposta Lista Autorizzazioni Email Risorsa",
|
"actionSetResourceEmailWhitelist": "Imposta Lista Autorizzazioni Email Risorsa",
|
||||||
"actionGetResourceEmailWhitelist": "Ottieni Lista Autorizzazioni Email Risorsa",
|
"actionGetResourceEmailWhitelist": "Ottieni Lista Autorizzazioni Email Risorsa",
|
||||||
|
"actionListResourcePolicies": "Elenca Politiche Risorsa",
|
||||||
|
"actionCreateResourcePolicy": "Crea Politica Risorsa",
|
||||||
"actionGetResourcePolicy": "Ottieni la Politica della Risorsa",
|
"actionGetResourcePolicy": "Ottieni la Politica della Risorsa",
|
||||||
"actionUpdateResourcePolicy": "Aggiorna la Politica della Risorsa",
|
"actionUpdateResourcePolicy": "Aggiorna la Politica della Risorsa",
|
||||||
|
"actionDeleteResourcePolicy": "Elimina Politica Risorsa",
|
||||||
"actionSetResourcePolicyUsers": "Imposta Utenti della Politica Risorsa",
|
"actionSetResourcePolicyUsers": "Imposta Utenti della Politica Risorsa",
|
||||||
"actionSetResourcePolicyRoles": "Imposta Ruoli della Politica Risorsa",
|
"actionSetResourcePolicyRoles": "Imposta Ruoli della Politica Risorsa",
|
||||||
"actionSetResourcePolicyPassword": "Imposta Password della Politica Risorsa",
|
"actionSetResourcePolicyPassword": "Imposta Password della Politica Risorsa",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Inviti",
|
"sidebarInvitations": "Inviti",
|
||||||
"sidebarRoles": "Ruoli",
|
"sidebarRoles": "Ruoli",
|
||||||
"sidebarShareableLinks": "Collegamenti Condivisibili",
|
"sidebarShareableLinks": "Collegamenti Condivisibili",
|
||||||
|
"sidebarAiGateway": "Gateway AI",
|
||||||
|
"sidebarAiProviders": "Provider",
|
||||||
|
"commandAiProviders": "Provider AI",
|
||||||
|
"sidebarVirtualApiKeys": "Chiavi API Virtuali",
|
||||||
|
"sidebarMyApiKeys": "Le Tue Chiavi API",
|
||||||
|
"sidebarAccount": "Launcher",
|
||||||
|
"commandVirtualApiKeys": "Chiavi API Virtuali",
|
||||||
|
"virtualApiKeysTitle": "Gestisci Chiavi API Virtuali",
|
||||||
|
"virtualApiKeysDescription": "Crea e gestisci chiavi API manuali per l'accesso ai gateway AI pubblici",
|
||||||
|
"virtualApiKeysTabIdentity": "Chiavi Identità",
|
||||||
|
"virtualApiKeysTabVirtual": "Chiavi Virtuali",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Chiavi di Identità per Ogni Utente",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Ogni utente ha già una chiave di identità Pangolin per questa organizzazione. È unica per il loro account e li autentica ai gateway AI a cui possono accedere.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Esempio",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Come Gli Utenti Ottengono La Loro Chiave",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Visita un URL di gateway AI pubblico nel browser e accedi con il tuo account.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Oppure vai su <url></url> mentre sei loggato.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Puoi creare ulteriori chiavi API virtuali nella scheda Chiavi Virtuali. Quelle chiavi possono essere limitate a specifici gateway AI pubblici e opzionalmente associate a un utente. La creazione di una chiave concede immediatamente l'accesso ai gateway AI pubblici selezionati.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Crea Manualmente una Chiave",
|
||||||
|
"virtualApiKeysEmailIdentity": "Email Chiavi Identità",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Invia a ogni utente o ruolo selezionato la loro chiave di identità Pangolin.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Invia a tutti gli utenti",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Invia un'email a ogni membro dell'organizzazione che ha un'email associata.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Utenti",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Ruoli",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Invia Email",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Chiavi di Identità Inviate Via Email",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Inviate {sent} email.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} utenti sono stati saltati perché non hanno un indirizzo email.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Seleziona almeno un utente o ruolo, oppure invia a tutti gli utenti.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Errore nell'invio delle chiavi di identità",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Impossibile inviare email chiavi di identità",
|
||||||
|
"virtualApiKeysBannerTitle": "Chiavi di Identità per Ogni Utente",
|
||||||
|
"virtualApiKeysBannerDescription": "Ogni utente ha già una chiave di identità disponibile su {keysUrl}. Puoi anche generare manualmente chiavi qui che concedono accesso diretto a gateway AI pubblici.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Visualizza Chiavi di Identità",
|
||||||
|
"virtualApiKeys": "Chiavi API Virtuali",
|
||||||
|
"virtualApiKeysSearch": "Cerca chiavi...",
|
||||||
|
"virtualApiKeysCreate": "Crea Chiave API Virtuale",
|
||||||
|
"virtualApiKeysCreateDescription": "Crea una chiave manuale che possa chiamare gateway AI pubblici in questa organizzazione",
|
||||||
|
"virtualApiKeysCreateButton": "Crea Chiave",
|
||||||
|
"virtualApiKeysEmpty": "Ancora nessuna chiave API virtuale",
|
||||||
|
"virtualApiKeysName": "Nome",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Descrizione (opzionale)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Associa Utente (opzionale)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Associa questa chiave a un utente per monitorare l'uso. Una volta creata, questa chiave concede immediatamente accesso ai gateway AI pubblici selezionati. Non è necessario associare manualmente un utente alla risorsa. La chiave verrà visualizzata anche nel profilo utente.",
|
||||||
|
"virtualApiKeysAllResources": "Tutti i gateway AI pubblici",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Consenti a questa chiave di accedere a tutti i gateway AI pubblici dell'organizzazione",
|
||||||
|
"virtualApiKeysSelectResources": "Gateway AI Pubblici",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Seleziona risorse",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Scegli quali gateway AI pubblici questa chiave può accedere",
|
||||||
|
"virtualApiKeysNoResources": "Nessuna risorsa",
|
||||||
|
"virtualApiKeysSecret": "Chiave",
|
||||||
|
"virtualApiKeysCopyKey": "Copia questa chiave. Puoi vederla di nuovo più tardi dalla tabella o quando la modifichi.",
|
||||||
|
"virtualApiKeysViewSecret": "Visualizza Segreto",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Segreto della Chiave API Virtuale",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Questo segreto concede l'accesso ai gateway AI pubblici assegnati a questa chiave",
|
||||||
|
"virtualApiKeysEdit": "Modifica Chiave API Virtuale",
|
||||||
|
"virtualApiKeysEditDescription": "Aggiorna l'utente associato e l'accesso ai gateway AI pubblici per questa chiave",
|
||||||
|
"virtualApiKeysSaveButton": "Salva Modifiche",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Seleziona almeno un gateway AI pubblico, o abilita tutti i gateway AI pubblici",
|
||||||
|
"virtualApiKeysUpdated": "Chiave API virtuale aggiornata",
|
||||||
|
"virtualApiKeysUpdatedDescription": "La chiave API virtuale è stata aggiornata",
|
||||||
|
"virtualApiKeysErrorUpdate": "Errore nell'aggiornamento della chiave API virtuale",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Impossibile aggiornare la chiave API virtuale",
|
||||||
|
"virtualApiKeysErrorCreate": "Errore nella creazione della chiave API virtuale",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Impossibile creare la chiave API virtuale",
|
||||||
|
"virtualApiKeysErrorDelete": "Errore nell'eliminazione della chiave API virtuale",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Impossibile eliminare la chiave API virtuale",
|
||||||
|
"virtualApiKeysDeleted": "Chiave API virtuale eliminata",
|
||||||
|
"virtualApiKeysDeletedDescription": "La chiave API virtuale è stata eliminata",
|
||||||
|
"virtualApiKeysDelete": "Elimina Chiave API Virtuale",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Elimina Chiave",
|
||||||
|
"virtualApiKeysQuestionRemove": "Sei sicuro di voler eliminare questa chiave API virtuale?",
|
||||||
|
"virtualApiKeysMessageRemove": "I client che usano questa chiave perderanno immediatamente l'accesso.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Errore nel caricamento del segreto",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Impossibile caricare il segreto della chiave API virtuale",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Non assegnato",
|
||||||
|
"virtualApiKeysInferenceBudget": "Budget",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Configura come questa chiave limita l'uso dell'AI in base alla spesa o ai limiti di token",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Email chiave alla generazione",
|
||||||
|
"virtualApiKeysEmailThisKey": "Email questa chiave",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Invia la chiave all'utente associato e ad indirizzi aggiuntivi dopo che è creata",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Invia la chiave attuale all'utente associato e ad indirizzi aggiuntivi",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "Email non configurata su questo server",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Configura SMTP per email chiavi API virtuali.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Invia all'utente associato",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Email della chiave all'email dell'account dell'utente associato",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Associa un utente per inviare la chiave a tale utente",
|
||||||
|
"virtualApiKeysEmailAdditional": "Email aggiuntive",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Aggiungi email e premi Invio",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Seleziona l'utente associato o aggiungi almeno un indirizzo email",
|
||||||
|
"myVirtualApiKeysTitle": "Le Tue Chiavi API",
|
||||||
|
"myVirtualApiKeysDescription": "Visualizza la tua chiave di identità e tutte le chiavi API virtuali attribuite a te in questa organizzazione",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Le Tue Chiavi API per {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Visualizza la tua chiave di identità e le chiavi API virtuali a te attribuite che possono accedere a {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Chiave di Identità",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "La Tua Chiave API Personale",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "La tua chiave personale per questa organizzazione. È unica per il tuo account e viene utilizzata per identificarti quando chiami le risorse Gateway AI.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "La Tua Chiave API Personale per {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "La tua chiave personale per questa organizzazione. Usala per chiamare {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Chiavi Attribuite",
|
||||||
|
"myVirtualApiKeysManualDescription": "Chiavi API virtuali manuali attribuite al tuo account",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Chiavi API virtuali manuali associate al tuo account che possono accedere a {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Ancora nessuna chiave attribuita",
|
||||||
|
"myVirtualApiKeysKindUser": "Identità",
|
||||||
|
"myVirtualApiKeysKindManual": "Manuale",
|
||||||
|
"myVirtualApiKeysUnnamed": "Chiave senza nome",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Rivela Segreto",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Questo segreto ti autentica alle risorse Gateway AI",
|
||||||
|
"aiProvidersTitle": "Provider AI",
|
||||||
|
"aiProvidersDescription": "Connetti provider di modelli per i carichi di lavoro AI in questa organizzazione",
|
||||||
|
"aiProvidersBannerTitle": "Connetti Provider di Modelli",
|
||||||
|
"aiProvidersBannerDescription": "I provider sono i backend dei modelli che Pangolin utilizza per i carichi di lavoro AI. Connetti OpenAI, Anthropic e altri provider qui, poi allegali ai gateway AI pubblici in modo che gli utenti possano chiamare modelli con accesso consapevole dell'identità, budget e registrazione.",
|
||||||
|
"aiProvidersAdd": "Aggiungi Provider",
|
||||||
|
"aiProvidersSearch": "Cerca provider...",
|
||||||
|
"aiProvidersEmpty": "Ancora nessun provider AI",
|
||||||
|
"aiProviderCreate": "Crea Provider AI",
|
||||||
|
"aiProviderCreateDescription": "Aggiungi un provider di modelli per questa organizzazione",
|
||||||
|
"aiProviderSeeAll": "Vedi Tutti i Provider",
|
||||||
|
"aiProviderSetting": "Impostazioni Provider per {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Configura questo provider AI",
|
||||||
|
"aiProviderGeneral": "Generale",
|
||||||
|
"aiProviderGeneralDescription": "Impostazioni di base per questo provider",
|
||||||
|
"aiProviderConfiguration": "Configurazione",
|
||||||
|
"aiProviderConfigurationDescription": "Instradamento di rete e autenticazione per questo provider",
|
||||||
|
"aiProviderNetworkSettings": "Impostazioni di Rete",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Scegli come raggiungere questo provider tramite il traffico",
|
||||||
|
"aiProviderAuthSettings": "Autenticazione",
|
||||||
|
"aiProviderAuthSettingsDescription": "Configura come questo provider autentica le richieste al suo URL a monte",
|
||||||
|
"aiProviderBudgetSettings": "Budget",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Configura come questo provider limita l'uso basato su spese o limiti di token",
|
||||||
|
"aiBudgetAdd": "Aggiungi Budget",
|
||||||
|
"aiBudgetEmpty": "Nessun budget ancora configurato. Clicca su Aggiungi Budget per impostare un limite di spesa o token.",
|
||||||
|
"aiBudgetUnit": "Tipo di Spesa",
|
||||||
|
"aiBudgetPeriod": "Periodo di Reset",
|
||||||
|
"aiBudgetAmount": "Spesa Massima",
|
||||||
|
"aiBudgetAmountPlaceholder": "Spesa massima",
|
||||||
|
"aiBudgetPeriodHourly": "Oraria",
|
||||||
|
"aiBudgetPeriodDaily": "Giornaliera",
|
||||||
|
"aiBudgetPeriodWeekly": "Settimanale",
|
||||||
|
"aiBudgetPeriodMonthly": "Mensile",
|
||||||
|
"aiBudgetPeriodYearly": "Annuale",
|
||||||
|
"aiBudgetPeriodLifetime": "A Vita",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Token",
|
||||||
|
"aiBudgetConflictError": "Esiste già un budget per questo periodo di reset e tipo di spesa",
|
||||||
|
"aiBudgetInvalidAmountError": "Inserisci una spesa massima superiore a 0",
|
||||||
|
"aiBudgetUpdated": "Budget aggiornati",
|
||||||
|
"aiBudgetErrorSave": "Impossibile aggiornare i budget",
|
||||||
|
"aiProviderType": "Tipo di Provider",
|
||||||
|
"aiProviderTypeSearch": "Cerca provider...",
|
||||||
|
"aiProviderTypeNotFound": "Nessun tipo di provider trovato",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Gateway AI Vercel",
|
||||||
|
"aiProviderTypeCustom": "Personalizzato",
|
||||||
|
"aiProviderTypeOpenaiDescription": "API di OpenAI",
|
||||||
|
"aiProviderTypeAnthropicDescription": "API di Anthropic",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "API di Google Gemini",
|
||||||
|
"aiProviderTypeVertexAiDescription": "API di Google Vertex AI",
|
||||||
|
"aiProviderTypeBedrockDescription": "API di Runtime di Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "API di Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "API di OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "API di Gateway AI Vercel",
|
||||||
|
"aiProviderTypeCustomDescription": "Porta il tuo endpoint o instrada tramite target del sito",
|
||||||
|
"aiProviderUpstreamUrl": "URL a monte",
|
||||||
|
"aiProviderUpstreamUrlDescription": "URL di base per l'API del provider",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Lascia vuoto per usare l'URL a monte predefinito per questo provider",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "URL effettivo a monte",
|
||||||
|
"aiProviderApiKey": "Chiave API",
|
||||||
|
"aiProviderApiKeyDescription": "Chiave API utilizzata per autenticare le richieste a questo provider",
|
||||||
|
"aiProviderCustomHeadersDescription": "Intestazioni inviate su ogni richiesta a questo fornitore. Separato da nuova riga: Nome-Intestazione: valore",
|
||||||
|
"aiProviderApiKeyLastChars": "Chiave API",
|
||||||
|
"aiProviderAuthType": "Tipo di Auth",
|
||||||
|
"aiProviderAuthTypeSearch": "Cerca tipi di auth...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Nessun tipo di auth trovato",
|
||||||
|
"aiProviderAuthTypeBearer": "Portatore",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Autorizzazione: chiave del portatore. Utilizzato da OpenAI e dalla maggior parte dei provider",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "intestazione x-api-key. Utilizzato da Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "intestazione x-goog-api-key. Utilizzato da Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Autorizzazione: chiave Splunk. Utilizzato dal Collettore di Eventi HTTP di Splunk",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-autorizzazione: chiave del portatore. Utilizzato da Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "Nessuna autentificazione",
|
||||||
|
"aiProviderAuthTypePassthrough": "Trasmissione diretta",
|
||||||
|
"aiProviderAuthTypeDescription": "Come l'API a monte autentica le richieste",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Trasmetti le intestazioni della chiave API del chiamante al monte",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Non inviare intestazioni di autenticazione all'a monte",
|
||||||
|
"aiProviderRoutingMode": "Modalità di instradamento",
|
||||||
|
"aiProviderRoutingModeDescription": "Invia il traffico a un URL a monte o a target HTTP sui tuoi siti",
|
||||||
|
"aiProviderRoutingModeUrl": "URL a monte",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Chiama un URL base API pubblico o privato",
|
||||||
|
"aiProviderRoutingModeTarget": "Target del sito",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Instrada tramite target sui tuoi siti",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Dopo aver creato questo provider, configura i target del sito nella scheda Impostazioni di Rete.",
|
||||||
|
"aiProviderTargetNoOne": "Questo provider non ha alcun target. Aggiungi un target per instradare le richieste attraverso i tuoi siti.",
|
||||||
|
"aiProviderSkipTlsVerification": "Salta la verifica TLS",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Disabilita la verifica del certificato TLS per la connessione a monte",
|
||||||
|
"aiProviderBudget": "Budget",
|
||||||
|
"aiProviderBudgetDescription": "Budget opzionale di spesa o di token per questo provider",
|
||||||
|
"aiProviderBudgetAmount": "Importo del budget",
|
||||||
|
"aiProviderBudgetUnit": "Unità di budget",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Token",
|
||||||
|
"aiProviderEnabled": "Abilitato",
|
||||||
|
"aiProviderEnabledDescription": "Disabilita completamente questo provider su tutte le risorse",
|
||||||
|
"aiProviderErrorCreate": "Impossibile creare il fornitore di AI",
|
||||||
|
"aiProviderErrorUpdate": "Impossibile aggiornare il fornitore di AI",
|
||||||
|
"aiProviderErrorDelete": "Impossibile eliminare il fornitore di AI",
|
||||||
|
"aiProviderErrorLoad": "Impossibile caricare il fornitore di AI",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Inserisci un URL a monte valido",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "L'URL a monte è richiesto per questo provider",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "È richiesto un tipo di autenticazione",
|
||||||
|
"aiProviderErrorApiKeyRequired": "È richiesta la chiave API",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "L'instradamento dei target del sito è disponibile solo per provider personalizzati",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Seleziona almeno una capacità API",
|
||||||
|
"aiProviderCapabilities": "Capacità dell'API",
|
||||||
|
"aiProviderCapabilitiesDescription": "Seleziona quale formato API questo fornitore può gestire. I fornitori noti iniziano con impostazioni predefinite raccomandate.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Seleziona quali formati API questo provider personalizzato può gestire",
|
||||||
|
"aiProviderCapabilitiesSelect": "Seleziona capacità",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Nessuna capacità trovata",
|
||||||
|
"aiProviderCapabilitiesSearch": "Cerca capacità...",
|
||||||
|
"aiCapabilityOpenaiChat": "Completamenti Chat OpenAI",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Supporta /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "Risposte OpenAI",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Supporta /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Messaggi Anthropic",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Supporta /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Generazione di Contenuti Gemini",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Supporta l'API diretta di Gemini",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Invoca Modello Bedrock",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Supporta Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Generazione di Contenuti Vertex",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Supporta il formato Vertex AI Gemini",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Predizione Grezza Vertex",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Supporta la predizione grezza di Vertex AI per i modelli Anthropic",
|
||||||
|
"aiCapabilityBedrockConverse": "Conversazione Bedrock",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Supporta l'API di Conversazione Amazon Bedrock",
|
||||||
|
"aiProviderCreated": "Provider AI creato",
|
||||||
|
"aiProviderUpdated": "Provider AI aggiornato",
|
||||||
|
"aiProviderDeleted": "Provider AI eliminato",
|
||||||
|
"aiProviderDelete": "Elimina Provider",
|
||||||
|
"aiProviderDeleteConfirm": "Elimina Provider",
|
||||||
|
"aiProviderQuestionRemove": "Sei sicuro di voler eliminare questo provider AI?",
|
||||||
|
"aiProviderMessageRemove": "Questo eliminerà permanentemente il provider e i suoi modelli e obiettivi. Questa azione non può essere annullata.",
|
||||||
|
"aiProviderErrorNoUpdate": "Il provider AI non è disponibile per l'aggiornamento",
|
||||||
|
"aiProviderModels": "Modelli",
|
||||||
|
"aiProviderModelsDescription": "Definisci le liste di autorizzazione e blocco per questo provider. Le richieste devono corrispondere a un'entrata di autorizzazione e non devono corrispondere a un'entrata di blocco.",
|
||||||
|
"aiProviderCreateModelsDescription": "Scegli quali modelli questo provider può servire. Una lista di autorizzazione vuota nega tutto il traffico. Puoi aggiungere o modificare i modelli in un secondo momento.",
|
||||||
|
"aiProviderModelsPlaceholder": "Cerca modelli o digita una chiave personalizzata",
|
||||||
|
"aiProviderModelsAllow": "Lista di Autorizzazione",
|
||||||
|
"aiProviderModelsAllowDescription": "Modelli che possono essere utilizzati tramite questo provider. Vuoto significa negare tutto.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Inserisci chiave modello",
|
||||||
|
"aiProviderModelsAllowEmpty": "Nessun modello aggiunto ancora.",
|
||||||
|
"aiProviderModelsBlock": "Lista di Blocco",
|
||||||
|
"aiProviderModelsBlockDescription": "Modelli da negare anche se corrispondono a un'entrata di autorizzazione.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Inserisci chiave modello",
|
||||||
|
"aiProviderModelsBlockEmpty": "Nessun modello aggiunto ancora.",
|
||||||
|
"aiProviderModelsAdd": "Aggiungi Modelli",
|
||||||
|
"aiProviderModelsClearAll": "Cancella Tutto",
|
||||||
|
"aiProviderModelsAddCustom": "Aggiungi \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Premi Invio per aggiungere questa chiave modello personalizzata.",
|
||||||
|
"aiProviderModelsAddBulk": "Aggiungi {count} chiavi personalizzate",
|
||||||
|
"aiProviderModelsAddBulkHint": "Premi Invio per aggiungere {count} chiavi personalizzate.",
|
||||||
|
"aiProviderModelsAddOne": "Aggiungi {key} ora",
|
||||||
|
"aiProviderModelsAddSelected": "Aggiungi Selezionati",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} selezionato",
|
||||||
|
"aiProviderModelsSelectAll": "Seleziona tutto",
|
||||||
|
"aiProviderModelsClearSelected": "Cancella",
|
||||||
|
"aiProviderModelsBulkHint": "Seleziona modelli conosciuti o digita una chiave personalizzata.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Nessun modello di catalogo corrispondente.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Modelli Conosciuti",
|
||||||
|
"aiProviderModelsAllLabel": "Tutti i modelli",
|
||||||
|
"aiProviderModelsAllPatternHint": "Jolly: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Consenti a tutti i modelli",
|
||||||
|
"aiProviderModelsAddAllBlock": "Blocca tutti i modelli",
|
||||||
|
"aiProviderModelsAddAllDescription": "Utilizza il carattere jolly * in modo che ogni chiave modello corrisponda.",
|
||||||
|
"aiProviderModelsViewMore": "Visualizza di più ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Visualizza meno",
|
||||||
|
"aiProviderModelsRemove": "Rimuovi modello",
|
||||||
|
"aiProviderModelsEditHint": "Fai clic per modificare le impostazioni del modello",
|
||||||
|
"aiProviderModelsSourceCatalog": "Modello di catalogo conosciuto",
|
||||||
|
"aiProviderModelsSourceCustom": "Chiave modello personalizzata",
|
||||||
|
"aiProviderModelsSourcePattern": "Modello Jolly",
|
||||||
|
"aiProviderModelsSourceAll": "Corrisponde a ogni chiave modello",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Budget configurato",
|
||||||
|
"aiProviderModelsEditTitle": "Modifica Modello",
|
||||||
|
"aiProviderModelsEditDescription": "Aggiorna la chiave del modello o configura il suo budget.",
|
||||||
|
"aiProviderModelsBudgetTab": "Budget",
|
||||||
|
"aiProviderModelsKeyLabel": "Chiave Modello",
|
||||||
|
"aiProviderModelsKeyRequired": "Inserisci una chiave modello",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Questa chiave modello è già in una lista",
|
||||||
|
"aiProviderModelsOverlapError": "Questi modelli non possono essere in entrambe le liste: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modelli aggiornati",
|
||||||
|
"aiProviderModelsErrorUpdate": "Impossibile aggiornare i modelli",
|
||||||
|
"aiResourceProviders": "Provider",
|
||||||
|
"aiResourceProvidersDescription": "Scegli quali provider AI questo gateway AI può utilizzare",
|
||||||
|
"aiResourceProvidersHelp": "Allega i provider e scegli eredita (usa le liste di ciascun provider) o seleziona (scegli una lista di autorizzazione per questa risorsa). Non sono ammessi schemi che confliggono tra i provider allegati.",
|
||||||
|
"aiResourceProvidersSelect": "Seleziona i provider",
|
||||||
|
"aiResourceProvidersEmpty": "Nessun provider AI trovato",
|
||||||
|
"aiResourceProvidersNoneAttached": "Nessun provider allegato ancora.",
|
||||||
|
"aiResourceProvidersAdd": "Aggiungi provider",
|
||||||
|
"aiResourceProvidersRemove": "Rimuovi provider",
|
||||||
|
"aiResourceProviderToggleEnabled": "Abilita o disabilita questo provider sulla risorsa",
|
||||||
|
"aiResourceProviderDisabled": "Disabilitato",
|
||||||
|
"aiResourceProvidersUpdated": "Provider aggiornati",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Impossibile aggiornare i provider",
|
||||||
|
"aiResourceProviderEditDescription": "Scegli come sono esposti i modelli di questo provider su questa risorsa.",
|
||||||
|
"viewProviderSettings": "Visualizza le Impostazioni del Provider",
|
||||||
|
"aiResourceProviderMode": "Modalità di Accesso",
|
||||||
|
"aiResourceProviderModeInherit": "Eredita",
|
||||||
|
"aiResourceProviderModeSelect": "Seleziona",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Seleziona · {count} modelli",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Utilizza le liste di autorizzazione e blocco di questo provider come configurato sul provider.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Scegli un sottoinsieme dei modelli della lista di autorizzazione di questo provider per questa risorsa.",
|
||||||
|
"aiResourceProviderAllowModels": "Lista di autorizzazione",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Seleziona modelli",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Cerca modelli...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Nessun modello trovato",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Solo i modelli dalla lista di autorizzazione di questo provider possono essere selezionati.",
|
||||||
|
"aiResourceAliasRequired": "Alias è richiesto per gateway AI",
|
||||||
|
"aiResourceDomainConfiguration": "Configurazione del Dominio",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Scegli il dominio che i clienti utilizzeranno per accedere a questo gateway AI.",
|
||||||
|
"aiUsageAnalyticsTitle": "Analisi dell'Uso AI",
|
||||||
|
"aiUsageAnalyticsDescription": "Analizza il costo del gateway AI, l'uso dei token e l'attività tra provider, risorse, ruoli e utenti",
|
||||||
|
"aiUsageTabOverview": "Panoramica",
|
||||||
|
"aiUsageTabProviders": "Provider",
|
||||||
|
"aiUsageTabResources": "Risorse",
|
||||||
|
"aiUsageFilterProvider": "Provider",
|
||||||
|
"aiUsageFilterModel": "Modello",
|
||||||
|
"aiUsageFilterResource": "Risorsa",
|
||||||
|
"aiUsageFilterRole": "Ruolo",
|
||||||
|
"aiUsageFilterUser": "Utente",
|
||||||
|
"aiUsageFilterAllProviders": "Tutti i Provider",
|
||||||
|
"aiUsageFilterAllModels": "Tutti i Modelli",
|
||||||
|
"aiUsageFilterAllResources": "Tutte le Risorse",
|
||||||
|
"aiUsageFilterAllRoles": "Tutti i Ruoli",
|
||||||
|
"aiUsageFilterAllUsers": "Tutti gli Utenti",
|
||||||
|
"aiUsageFilterSearch": "Ricerca...",
|
||||||
|
"aiUsageFilterNotFound": "Nessuna opzione trovata",
|
||||||
|
"aiUsageResetFilters": "Ripristina Filtri",
|
||||||
|
"aiUsageRefresh": "Ricarica",
|
||||||
|
"aiUsageTokenTypePrompt": "Prompt",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Leggi Cache",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Scrivi Cache",
|
||||||
|
"aiUsageTokenTypeCompletion": "Completamento",
|
||||||
|
"aiUsageTokenTypeReasoning": "Ragionamento",
|
||||||
|
"aiUsageRequests": "Richieste",
|
||||||
|
"aiUsageCost": "Costo",
|
||||||
|
"aiUsageTokens": "Token",
|
||||||
|
"aiUsageOther": "Altro",
|
||||||
|
"aiUsageTotalRequests": "Richieste Totali",
|
||||||
|
"aiUsageTotalTokens": "Token Totali",
|
||||||
|
"aiUsageTotalCost": "Costo Totale",
|
||||||
|
"aiUsageEstimated": "Stimato",
|
||||||
|
"aiUsageRequestVolume": "Volume delle Richieste",
|
||||||
|
"aiUsageTokenUsage": "Uso dei Token",
|
||||||
|
"aiUsageModelCost": "Costo del Modello",
|
||||||
|
"aiUsageModelTokens": "Token del Modello",
|
||||||
|
"aiUsageTopModels": "Migliori Modelli",
|
||||||
|
"aiUsageTopProviders": "Migliori Provider",
|
||||||
|
"aiUsageProviderCost": "Costo del Provider",
|
||||||
|
"aiUsageProviderTokenUsage": "Uso del Token del Provider",
|
||||||
|
"aiUsageTopResources": "Migliori Risorse",
|
||||||
|
"aiUsageResourceCost": "Costo della Risorsa",
|
||||||
|
"aiUsageResourceTokenUsage": "Uso del Token della Risorsa",
|
||||||
|
"aiUsageResourceTypePublic": "Risorsa Pubblica",
|
||||||
|
"aiUsageResourceTypeSite": "Risorsa Privata",
|
||||||
|
"aiUsageNoResource": "Nessuna risorsa",
|
||||||
|
"aiUsageRolesTab": "Ruoli",
|
||||||
|
"aiUsageUsersTab": "Utenti",
|
||||||
|
"aiUsageTopRoles": "Migliori Ruoli",
|
||||||
|
"aiUsageRoleCost": "Costo Ruolo",
|
||||||
|
"aiUsageRoleTokenUsage": "Uso del Token Ruolo",
|
||||||
|
"aiUsageTopUsers": "Migliori Utenti",
|
||||||
|
"aiUsageUserCost": "Costo Utente",
|
||||||
|
"aiUsageUserTokenUsage": "Uso del Token Utente",
|
||||||
|
"aiUsageUnknownUser": "Utente sconosciuto",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Chiavi API Virtuali",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Chiave API Virtuale",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Tutte le Chiavi API Virtuali",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Migliori Chiavi API Virtuali",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Costo Chiave API Virtuale",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Uso del Token Chiave API Virtuale",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Nessuna chiave API virtuale",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Chiave senza nome",
|
||||||
|
"aiUsageLoading": "Caricamento...",
|
||||||
|
"aiUsageNoData": "Nessun dato",
|
||||||
|
"resourceBudgetSettings": "Budget",
|
||||||
|
"resourceBudgetSettingsDescription": "Configura come questo gateway AI limita l'uso basato sulla spesa o sui limiti di token",
|
||||||
"sidebarApiKeys": "Chiavi API",
|
"sidebarApiKeys": "Chiavi API",
|
||||||
"sidebarProvisioning": "Accantonamento",
|
"sidebarProvisioning": "Accantonamento",
|
||||||
"sidebarSettings": "Impostazioni",
|
"sidebarSettings": "Impostazioni",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Sottorete",
|
"subnetPlaceholder": "Sottorete",
|
||||||
"addressDescription": "L'indirizzo interno del client. Deve rientrare nella sottorete dell'organizzazione.",
|
"addressDescription": "L'indirizzo interno del client. Deve rientrare nella sottorete dell'organizzazione.",
|
||||||
"selectSites": "Seleziona siti",
|
"selectSites": "Seleziona siti",
|
||||||
|
"selectResources": "Seleziona risorse",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# risorsa} other {# risorse}}",
|
||||||
"selectLabels": "Seleziona etichette",
|
"selectLabels": "Seleziona etichette",
|
||||||
"sitesDescription": "Il cliente avrà connettività ai siti selezionati",
|
"sitesDescription": "Il cliente avrà connettività ai siti selezionati",
|
||||||
"clientInstallOlm": "Installa Olm",
|
"clientInstallOlm": "Installa Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Il tempo è in secondi",
|
"timeIsInSeconds": "Il tempo è in secondi",
|
||||||
"requireDeviceApproval": "Richiede Approvazioni Dispositivo",
|
"requireDeviceApproval": "Richiede Approvazioni Dispositivo",
|
||||||
"requireDeviceApprovalDescription": "Gli utenti con questo ruolo hanno bisogno di nuovi dispositivi approvati da un amministratore prima di poter connettersi e accedere alle risorse.",
|
"requireDeviceApprovalDescription": "Gli utenti con questo ruolo hanno bisogno di nuovi dispositivi approvati da un amministratore prima di poter connettersi e accedere alle risorse.",
|
||||||
"sshSettings": "Impostazioni SSH",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI Gateway",
|
||||||
"sshAccess": "Accesso SSH",
|
"sshAccess": "Accesso SSH",
|
||||||
"rdpSettings": "Impostazioni RDP",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "Impostazioni VNC",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "Server SSH",
|
"sshServer": "Server SSH",
|
||||||
"rdpServer": "Server RDP",
|
"rdpServer": "Server RDP",
|
||||||
"vncServer": "Server VNC",
|
"vncServer": "Server VNC",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI Gateway",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Metodo HTTP",
|
"editInternalResourceDialogScheme": "Metodo HTTP",
|
||||||
"editInternalResourceDialogEnableSsl": "Abilita TLS",
|
"editInternalResourceDialogEnableSsl": "Abilita TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Consenti ICMP Ping",
|
"privateResourceAllowIcmpPing": "Consenti ICMP Ping",
|
||||||
"privateResourceNetworkAccess": "Accesso alla Rete",
|
"privateResourceNetworkAccess": "Accesso alla Rete",
|
||||||
"privateResourceNetworkAccessDescription": "Controlla l'accesso alle porte TCP/UDP e se ICMP ping è consentito per questa risorsa.",
|
"privateResourceNetworkAccessDescription": "Controlla l'accesso alle porte TCP/UDP e se ICMP ping è consentito per questa risorsa.",
|
||||||
"hostSettings": "Impostazioni dell'Host",
|
"hostSettings": "Host",
|
||||||
"cidrSettings": "Impostazioni CIDR",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Proprietà della Risorsa",
|
"createInternalResourceDialogResourceProperties": "Proprietà della Risorsa",
|
||||||
"createInternalResourceDialogName": "Nome",
|
"createInternalResourceDialogName": "Nome",
|
||||||
"createInternalResourceDialogSite": "Sito",
|
"createInternalResourceDialogSite": "Sito",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI Gateway",
|
||||||
"scheme": "Metodo HTTP",
|
"scheme": "Metodo HTTP",
|
||||||
"createInternalResourceDialogScheme": "Metodo HTTP",
|
"createInternalResourceDialogScheme": "Metodo HTTP",
|
||||||
"createInternalResourceDialogEnableSsl": "Abilita TLS",
|
"createInternalResourceDialogEnableSsl": "Abilita TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Password Valida",
|
"validPassword": "Password Valida",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Chiave API Virtuale Valida",
|
||||||
"view": "Visualizza",
|
"view": "Visualizza",
|
||||||
"configManaged": "Gestione Configurazione",
|
"configManaged": "Gestione Configurazione",
|
||||||
"connectedClient": "Cliente Connesso",
|
"connectedClient": "Cliente Connesso",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Ritenzione Registro",
|
"logRetention": "Ritenzione Registro",
|
||||||
"logRetentionDescription": "Gestisci per quanto tempo i diversi tipi di log sono mantenuti per questa organizzazione o disabilitali",
|
"logRetentionDescription": "Gestisci per quanto tempo i diversi tipi di log sono mantenuti per questa organizzazione o disabilitali",
|
||||||
"requestLogsDescription": "Visualizza i registri di richiesta dettagliati per le risorse in questa organizzazione",
|
"requestLogsDescription": "Visualizza i registri di richiesta dettagliati per le risorse in questa organizzazione",
|
||||||
|
"aiSessionLogs": "Log delle Sessioni AI Gateway",
|
||||||
|
"aiSessionLogsDescription": "Visualizza trascrizioni di prompt e risposte per le richieste del gateway AI in questa organizzazione",
|
||||||
|
"sidebarLogsAi": "Log delle Sessioni",
|
||||||
|
"commandLogsAi": "Log delle Sessioni",
|
||||||
|
"sidebarLogsAiUsage": "Analytics sull'Uso",
|
||||||
|
"commandLogsAiUsage": "Analytics sull'Uso",
|
||||||
|
"provider": "Provider",
|
||||||
|
"capability": "Capacità",
|
||||||
|
"model": "Modello",
|
||||||
|
"virtualApiKey": "Chiave API Virtuale",
|
||||||
|
"noVirtualApiKey": "Nessuna chiave API virtuale",
|
||||||
|
"stream": "Flusso",
|
||||||
|
"streaming": "Streaming",
|
||||||
|
"nonStreaming": "Non-streaming",
|
||||||
|
"statusCode": "Codice di Stato",
|
||||||
|
"aiSessionId": "ID Sessione",
|
||||||
|
"aiSessionRequest": "Richiesta",
|
||||||
|
"aiSessionResponse": "Risposta",
|
||||||
|
"aiSessionNoData": "Nessun dato catturato",
|
||||||
|
"aiSessionCouldNotParse": "(grezzo, non è stato possibile analizzare la trascrizione)",
|
||||||
|
"aiSessionLogTruncated": "Questa sessione è stata troncata prima dell'archiviazione e potrebbe essere incompleta.",
|
||||||
|
"aiSessionViewRaw": "Visualizza JSON Grezzo",
|
||||||
|
"aiSessionViewChat": "Visualizza Chat",
|
||||||
|
"cost": "Costo",
|
||||||
|
"estimated": "Stimato",
|
||||||
|
"tokenUsage": "Uso del Token",
|
||||||
|
"promptTokens": "Token Prompt",
|
||||||
|
"cacheReadTokens": "Token Lettura Cache",
|
||||||
|
"cacheWriteTokens": "Token Scrittura Cache",
|
||||||
|
"completionTokens": "Token Completamento",
|
||||||
|
"reasoningTokens": "Token Ragionamento",
|
||||||
|
"totalTokens": "Token Totali",
|
||||||
"requestAnalyticsDescription": "Visualizza le analisi dettagliate della richiesta per le risorse in questa organizzazione",
|
"requestAnalyticsDescription": "Visualizza le analisi dettagliate della richiesta per le risorse in questa organizzazione",
|
||||||
"logRetentionRequestLabel": "Conservazione Log Richieste HTTP",
|
"logRetentionRequestLabel": "Conservazione Log Richieste HTTP",
|
||||||
"logRetentionRequestDescription": "Per quanto tempo conservare i log delle richieste",
|
"logRetentionRequestDescription": "Per quanto tempo conservare i log delle richieste",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Per quanto tempo conservare i log delle azioni",
|
"logRetentionActionDescription": "Per quanto tempo conservare i log delle azioni",
|
||||||
"logRetentionConnectionLabel": "Ritenzione Registro Di Connessione",
|
"logRetentionConnectionLabel": "Ritenzione Registro Di Connessione",
|
||||||
"logRetentionConnectionDescription": "Per quanto tempo conservare i log di connessione",
|
"logRetentionConnectionDescription": "Per quanto tempo conservare i log di connessione",
|
||||||
|
"logRetentionAISessionsLabel": "Conservazione Log delle Sessioni AI Gateway",
|
||||||
|
"logRetentionAISessionsDescription": "Per quanto tempo mantenere i log delle sessioni prompt/risposta del gateway AI",
|
||||||
"logRetentionDisabled": "Disabilitato",
|
"logRetentionDisabled": "Disabilitato",
|
||||||
"logRetention3Days": "3 giorni",
|
"logRetention3Days": "3 giorni",
|
||||||
"logRetention7Days": "7 giorni",
|
"logRetention7Days": "7 giorni",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Indirizzo Di Origine",
|
"sourceAddress": "Indirizzo Di Origine",
|
||||||
"destinationAddress": "Indirizzo Di Destinazione",
|
"destinationAddress": "Indirizzo Di Destinazione",
|
||||||
"duration": "Durata",
|
"duration": "Durata",
|
||||||
"licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Prenota una demo gratuita o una prova POC per saperne di più.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Licenza Enterprise Edition</enterpriseEditionLink> è richiesta. <bookADemoLink>Prenota una demo o prova</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "L' <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> è necessaria per utilizzare questa funzione. Questa funzione è disponibile anche in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Prenota una demo gratuita o una prova POC per saperne di più.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Licenza Enterprise Edition</enterpriseEditionLink> è richiesta. <bookADemoLink>Prenota una demo o prova</bookADemoLink>",
|
||||||
"certResolver": "Risolutore Di Certificato",
|
"certResolver": "Risolutore Di Certificato",
|
||||||
"certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.",
|
"certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.",
|
||||||
"selectCertResolver": "Seleziona Risolutore Di Certificato",
|
"selectCertResolver": "Seleziona Risolutore Di Certificato",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Selezionare più siti consente un routing resiliente e Failover per alta disponibilità.",
|
"internalResourceFormMultiSiteRoutingHelp": "Selezionare più siti consente un routing resiliente e Failover per alta disponibilità.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Scopri di più",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Scopri di più",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Limita l'accesso a porte TCP/UDP specifiche o consenti/blocca tutte le porte.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Limita l'accesso a porte TCP/UDP specifiche o consenti/blocca tutte le porte.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "Configurazione HTTP",
|
"createInternalResourceDialogHttpConfiguration": "Configurazione del Dominio",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Scegli il dominio che i clienti utilizzeranno per accedere a questa risorsa tramite HTTP o HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Scegli il dominio che i clienti utilizzeranno per accedere a questa risorsa tramite HTTP o HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "Configurazione HTTP",
|
"editInternalResourceDialogHttpConfiguration": "Configurazione del Dominio",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Scegli il dominio che i clienti utilizzeranno per accedere a questa risorsa tramite HTTP o HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Scegli il dominio che i clienti utilizzeranno per accedere a questa risorsa tramite HTTP o HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Non Etichettato",
|
"resourceLauncherUnlabeled": "Non Etichettato",
|
||||||
|
"resourceLauncherAiGateway": "AI Gateway",
|
||||||
"resourceLauncherNoSite": "Nessun Sito",
|
"resourceLauncherNoSite": "Nessun Sito",
|
||||||
|
"resourceLauncherAvailableModels": "Modelli Disponibili",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modelli che puoi usare con questo gateway AI.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Nessun modello è disponibile per questa risorsa.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Impossibile caricare i modelli disponibili.",
|
||||||
|
"resourceLauncherApiKeys": "Chiavi API",
|
||||||
|
"resourceLauncherApiKeysDescription": "Usa la tua chiave di identità o una chiave attribuita per autenticarti con questa risorsa.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Chiave di Identità",
|
||||||
|
"resourceLauncherApiKeysManual": "Chiavi Attribuite",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Nessuna chiave API è disponibile per questa risorsa.",
|
||||||
|
"resourceLauncherApiKeysError": "Impossibile caricare le chiavi API.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Nessuna risorsa in questo gruppo",
|
"resourceLauncherNoResourcesInGroup": "Nessuna risorsa in questo gruppo",
|
||||||
"resourceLauncherEmptyStateTitle": "Non ci sono risorse disponibili",
|
"resourceLauncherEmptyStateTitle": "Non ci sono risorse disponibili",
|
||||||
"resourceLauncherEmptyStateDescription": "Non hai ancora accesso a nessuna risorsa. Contatta il tuo amministratore per richiedere l'accesso.",
|
"resourceLauncherEmptyStateDescription": "Non hai ancora accesso a nessuna risorsa. Contatta il tuo amministratore per richiedere l'accesso.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Non è stato possibile eliminare la visualizzazione del lanscia. Per favore riprova.",
|
"resourceLauncherViewDeleteFailedDescription": "Non è stato possibile eliminare la visualizzazione del lanscia. Per favore riprova.",
|
||||||
"memberPortalPrevious": "Precedente",
|
"memberPortalPrevious": "Precedente",
|
||||||
"memberPortalNext": "Successivo",
|
"memberPortalNext": "Successivo",
|
||||||
"httpSettings": "Impostazioni HTTP",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "Impostazioni TCP",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "Impostazioni UDP",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Stabilire una connessione sicura…",
|
"sshConnectingDescription": "Stabilire una connessione sicura…",
|
||||||
"sshConnecting": "Connessione…",
|
"sshConnecting": "Connessione…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "올바른 사용자로 로그인했는지 확인하십시오.",
|
"inviteLoginUser": "올바른 사용자로 로그인했는지 확인하십시오.",
|
||||||
"inviteErrorNoUser": "죄송하지만, 접근하려는 초대가 존재하지 않는 사용자에 대한 것인 것 같습니다.",
|
"inviteErrorNoUser": "죄송하지만, 접근하려는 초대가 존재하지 않는 사용자에 대한 것인 것 같습니다.",
|
||||||
"inviteCreateUser": "먼저 계정을 생성해 주세요.",
|
"inviteCreateUser": "먼저 계정을 생성해 주세요.",
|
||||||
|
"inviteErrorOidcNotAllowed": "초대는 내부 계정만 수락할 수 있습니다. 이 이메일의 비밀번호로 로그아웃하고 다시 로그인하십시오.",
|
||||||
|
"inviteLoginInternalOnly": "초대에는 비밀번호가 있는 내부 계정이 필요합니다. 계정을 생성하거나 비밀번호로 로그인하십시오.",
|
||||||
"goHome": "홈으로 가기",
|
"goHome": "홈으로 가기",
|
||||||
"inviteLogInOtherUser": "다른 사용자로 로그인",
|
"inviteLogInOtherUser": "다른 사용자로 로그인",
|
||||||
"createAnAccount": "계정 만들기",
|
"createAnAccount": "계정 만들기",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "국가를 찾을 수 없습니다.",
|
"noCountryFound": "국가를 찾을 수 없습니다.",
|
||||||
"siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.",
|
"siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.",
|
||||||
"resourceType": "리소스 유형",
|
"resourceType": "리소스 유형",
|
||||||
"resourceTypeDescription": "이것은 리소스 프로토콜 및 브라우저에서 렌더링되는 방식에 영향을 줍니다. 나중에 변경할 수 없습니다.",
|
"resourceTypeDescription": "이 리소스 프로토콜을 제어하며 나중에 변경할 수 없습니다.",
|
||||||
|
"resourceTypeSearch": "리소스 유형 검색...",
|
||||||
|
"resourceTypeNotFound": "리소스 유형이 발견되지 않았습니다",
|
||||||
|
"resourceTypeHttpDescription": "도메인 이름을 사용하여 HTTPS를 통한 웹 트래픽 프록시",
|
||||||
|
"resourceTypeInferenceDescription": "연결된 공급자를 통해 AI 요청을 라우팅",
|
||||||
|
"resourceTypeSshDescription": "브라우저에서 SSH 서버에 액세스",
|
||||||
|
"resourceTypeRdpDescription": "브라우저에서 원격 데스크톱에 액세스",
|
||||||
|
"resourceTypeVncDescription": "브라우저에서 VNC 데스크톱에 액세스",
|
||||||
|
"resourceTypeTcpDescription": "포트 번호를 사용하여 원시 TCP 트래픽 프록시",
|
||||||
|
"resourceTypeUdpDescription": "포트 번호를 사용하여 원시 UDP 트래픽 프록시",
|
||||||
|
"privateResourceTypeDescription": "클라이언트가 리소스에 도달하는 방법을 제어합니다. 나중에 변경할 수 없습니다.",
|
||||||
|
"privateResourceTypeHostDescription": "연결된 클라이언트에게 사이트 네트워크에서 단일 호스트를 노출",
|
||||||
|
"privateResourceTypeCidrDescription": "연결된 클라이언트에게 사이트 네트워크에서 CIDR 범위를 노출",
|
||||||
|
"privateResourceTypeHttpDescription": "도메인을 통해 HTTP 또는 HTTPS 서비스에 액세스",
|
||||||
|
"privateResourceTypeSshDescription": "연결된 클라이언트에서 SSH 서버에 액세스",
|
||||||
"resourceDomainDescription": "리소스는 숙주 네임에서 제공됩니다.",
|
"resourceDomainDescription": "리소스는 숙주 네임에서 제공됩니다.",
|
||||||
"resourceHTTPSSettings": "HTTPS 설정",
|
"resourceHTTPSSettings": "HTTPS 설정",
|
||||||
"resourceHTTPSSettingsDescription": "리소스가 HTTPS로 접근할 수 있는 방식을 구성합니다.",
|
"resourceHTTPSSettingsDescription": "리소스가 HTTPS로 접근할 수 있는 방식을 구성합니다.",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "설정 저장",
|
"authMethodsSave": "설정 저장",
|
||||||
"policyAuthStackTitle": "인증",
|
"policyAuthStackTitle": "인증",
|
||||||
"policyAuthStackDescription": "이 리소스에 접근하려면 어떤 인증 방법이 필요한지 제어합니다",
|
"policyAuthStackDescription": "이 리소스에 접근하려면 어떤 인증 방법이 필요한지 제어합니다",
|
||||||
|
"policyAuthInferenceStackDescription": "이 AI 게이트웨이에 인증할 수 있는 사용자 및 역할을 선택하십시오",
|
||||||
"policyAuthOrLogicTitle": "다수의 인증 방법 활성화",
|
"policyAuthOrLogicTitle": "다수의 인증 방법 활성화",
|
||||||
"policyAuthOrLogicBanner": "방문자는 아래 활성화된 방법 중 하나만을 선택하여 인증할 수 있습니다. 모든 방법을 완료할 필요는 없습니다.",
|
"policyAuthOrLogicBanner": "방문자는 아래 활성화된 방법 중 하나만을 선택하여 인증할 수 있습니다. 모든 방법을 완료할 필요는 없습니다.",
|
||||||
"policyAuthMethodActive": "활성화",
|
"policyAuthMethodActive": "활성화",
|
||||||
"policyAuthMethodOff": "비활성화",
|
"policyAuthMethodOff": "비활성화",
|
||||||
"policyAuthSsoTitle": "플랫폼 SSO",
|
"policyAuthSsoTitle": "플랫폼 SSO",
|
||||||
"policyAuthSsoDescription": "사용자의 아이덴티티 공급자를 통해 로그인 필요",
|
"policyAuthSsoDescription": "사용자의 아이덴티티 공급자를 통해 로그인 필요",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "로그인 URL",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "모든 사용자는 이미 ID API 키를 보유하고 있으므로 비사용자 클라이언트 또는 공유 액세스를 위한 가상 API 키만 생성하면 됩니다. 사용자는 이 리소스의 URL에서 ID 공급자로 로그인하면 키를 검색할 수 있습니다.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "모든 사용자는 이미 ID API 키를 보유하고 있으므로 비사용자 클라이언트 또는 공유 액세스를 위한 가상 API 키만 생성하면 됩니다. 사용자는 여기서 ID 공급자로 로그인 후 키를 검색할 수 있습니다.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} 사용자, {roles} 역할",
|
"policyAuthSsoSummary": "{idp} · {users} 사용자, {roles} 역할",
|
||||||
"policyAuthSsoDefaultIdp": "기본 공급자",
|
"policyAuthSsoDefaultIdp": "기본 공급자",
|
||||||
"policyAuthAddDefaultIdentityProvider": "기본 아이덴티티 공급자 추가",
|
"policyAuthAddDefaultIdentityProvider": "기본 아이덴티티 공급자 추가",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "새 역할이 필요합니다.",
|
"accessRoleErrorNewRequired": "새 역할이 필요합니다.",
|
||||||
"accessRoleErrorRemove": "역할 제거에 실패했습니다.",
|
"accessRoleErrorRemove": "역할 제거에 실패했습니다.",
|
||||||
"accessRoleErrorRemoveDescription": "역할을 제거하는 동안 오류가 발생했습니다.",
|
"accessRoleErrorRemoveDescription": "역할을 제거하는 동안 오류가 발생했습니다.",
|
||||||
|
"accessRoleInferenceBudget": "AI 예산",
|
||||||
|
"accessRoleInferenceBudgetDescription": "이 역할의 구성원이 AI 사용을 지출 또는 토큰 제한에 따라 제한하는 방법을 구성하십시오",
|
||||||
"accessRoleName": "역할 이름",
|
"accessRoleName": "역할 이름",
|
||||||
"accessRoleQuestionRemove": "`{name}` 역할을 삭제하려고 합니다. 이 작업은 되돌릴 수 없습니다.",
|
"accessRoleQuestionRemove": "`{name}` 역할을 삭제하려고 합니다. 이 작업은 되돌릴 수 없습니다.",
|
||||||
"accessRoleRemove": "역할 제거",
|
"accessRoleRemove": "역할 제거",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "사이트 삭제",
|
"actionDeleteSite": "사이트 삭제",
|
||||||
"actionGetSite": "사이트 가져오기",
|
"actionGetSite": "사이트 가져오기",
|
||||||
"actionListSites": "사이트 목록",
|
"actionListSites": "사이트 목록",
|
||||||
|
"actionCreateAiProvider": "AI 공급자 생성",
|
||||||
|
"actionDeleteAiProvider": "AI 공급자 삭제",
|
||||||
|
"actionGetAiProvider": "AI 공급자 가져오기",
|
||||||
|
"actionListAiProviders": "AI 공급자 목록",
|
||||||
|
"actionUpdateAiProvider": "AI 공급자 업데이트",
|
||||||
|
"actionCreateAiModel": "AI 모델 생성",
|
||||||
|
"actionDeleteAiModel": "AI 모델 삭제",
|
||||||
|
"actionGetAiModel": "AI 모델 가져오기",
|
||||||
|
"actionListAiModels": "AI 모델 목록",
|
||||||
|
"actionUpdateAiModel": "AI 모델 업데이트",
|
||||||
|
"actionCreateAiBudget": "AI 예산 생성",
|
||||||
|
"actionDeleteAiBudget": "AI 예산 삭제",
|
||||||
|
"actionGetAiBudget": "AI 예산 가져오기",
|
||||||
|
"actionListAiBudgets": "AI 예산 목록",
|
||||||
|
"actionUpdateAiBudget": "AI 예산 업데이트",
|
||||||
|
"actionListResourceAiModels": "리소스 AI 모델 목록",
|
||||||
|
"actionSetResourceAiModels": "리소스 AI 모델 설정",
|
||||||
|
"actionCreateVirtualApiKey": "가상 API 키 생성",
|
||||||
|
"actionDeleteVirtualApiKey": "가상 API 키 삭제",
|
||||||
|
"actionGetVirtualApiKey": "가상 API 키 가져오기",
|
||||||
|
"actionListVirtualApiKeys": "가상 API 키 목록",
|
||||||
|
"actionUpdateVirtualApiKey": "가상 API 키 업데이트",
|
||||||
"actionApplyBlueprint": "청사진 적용",
|
"actionApplyBlueprint": "청사진 적용",
|
||||||
"actionListBlueprints": "청사진 목록",
|
"actionListBlueprints": "청사진 목록",
|
||||||
"actionGetBlueprint": "청사진 가져오기",
|
"actionGetBlueprint": "청사진 가져오기",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "리소스 핀코드 설정",
|
"actionSetResourcePincode": "리소스 핀코드 설정",
|
||||||
"actionSetResourceEmailWhitelist": "리소스 이메일 화이트리스트 설정",
|
"actionSetResourceEmailWhitelist": "리소스 이메일 화이트리스트 설정",
|
||||||
"actionGetResourceEmailWhitelist": "리소스 이메일 화이트리스트 가져오기",
|
"actionGetResourceEmailWhitelist": "리소스 이메일 화이트리스트 가져오기",
|
||||||
|
"actionListResourcePolicies": "리소스 정책 목록",
|
||||||
|
"actionCreateResourcePolicy": "리소스 정책 생성",
|
||||||
"actionGetResourcePolicy": "리소스 정책 가져오기",
|
"actionGetResourcePolicy": "리소스 정책 가져오기",
|
||||||
"actionUpdateResourcePolicy": "리소스 정책 업데이트",
|
"actionUpdateResourcePolicy": "리소스 정책 업데이트",
|
||||||
|
"actionDeleteResourcePolicy": "리소스 정책 삭제",
|
||||||
"actionSetResourcePolicyUsers": "리소스 정책 사용자 설정",
|
"actionSetResourcePolicyUsers": "리소스 정책 사용자 설정",
|
||||||
"actionSetResourcePolicyRoles": "리소스 정책 역할 설정",
|
"actionSetResourcePolicyRoles": "리소스 정책 역할 설정",
|
||||||
"actionSetResourcePolicyPassword": "리소스 정책 비밀번호 설정",
|
"actionSetResourcePolicyPassword": "리소스 정책 비밀번호 설정",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "초대",
|
"sidebarInvitations": "초대",
|
||||||
"sidebarRoles": "역할",
|
"sidebarRoles": "역할",
|
||||||
"sidebarShareableLinks": "공유 가능한 링크",
|
"sidebarShareableLinks": "공유 가능한 링크",
|
||||||
|
"sidebarAiGateway": "AI 게이트웨이",
|
||||||
|
"sidebarAiProviders": "공급자",
|
||||||
|
"commandAiProviders": "AI 공급자",
|
||||||
|
"sidebarVirtualApiKeys": "가상 API 키",
|
||||||
|
"sidebarMyApiKeys": "귀하의 API 키",
|
||||||
|
"sidebarAccount": "실행기",
|
||||||
|
"commandVirtualApiKeys": "가상 API 키",
|
||||||
|
"virtualApiKeysTitle": "가상 API 키 관리",
|
||||||
|
"virtualApiKeysDescription": "공용 AI 게이트웨이에 대한 AI 게이트웨이 액세스를 위한 수동 API 키 생성 및 관리",
|
||||||
|
"virtualApiKeysTabIdentity": "신원 키",
|
||||||
|
"virtualApiKeysTabVirtual": "가상 키",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "모든 사용자에 대한 신원 키",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "이 조직을 위한 Pangolin 신원 키가 모든 사용자에게 이미 있습니다. 계정에 고유하며 액세스할 수 있는 AI 게이트웨이에 인증합니다.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "예시",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "사용자가 키 받는 방법",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "브라우저에서 공용 AI 게이트웨이 URL을 방문하고 계정으로 로그인합니다.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "<url></url>을 방문하여 로그인한 상태에서 이동하십시오.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "추가 가상 API 키를 가상 키 탭에서 생성할 수 있습니다. 이러한 키는 특정 공용 AI 게이트웨이에 범위를 지정할 수 있으며, 선택적으로 사용자와 연결할 수 있습니다. 키 생성 즉시 선택된 공용 AI 게이트웨이에 대한 액세스 권한이 부여됩니다.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "키 수동 생성",
|
||||||
|
"virtualApiKeysEmailIdentity": "이메일 신원 키",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "선택한 각 사용자 또는 역할에게 Pangolin 신원 키 보내기.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "모든 사용자에게 보내기",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "계정 이메일이 있는 모든 조직 구성원에게 이메일 발송.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "사용자",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "역할",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "이메일 보내기",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "신원 키 전송",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "{sent}개의 이메일이 전송되었습니다.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped}명의 사용자가 이메일 주소가 없어 건너뛰었습니다.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "최소한 하나의 사용자 또는 역할을 선택하거나 모든 사용자에게 보냅니다.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "신원 키 전송 오류",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "신원 키 이메일 전송 실패",
|
||||||
|
"virtualApiKeysBannerTitle": "모든 사용자에 대한 신원 키",
|
||||||
|
"virtualApiKeysBannerDescription": "모든 사용자는 {keysUrl}에서 신원 키를 이미 사용할 수 있습니다. 공용 AI 게이트웨이에 대한 직접 액세스를 부여하는 키를 수동으로 여기에서 생성할 수도 있습니다.",
|
||||||
|
"virtualApiKeysBannerButtonText": "신원 키 보기",
|
||||||
|
"virtualApiKeys": "가상 API 키",
|
||||||
|
"virtualApiKeysSearch": "키 검색...",
|
||||||
|
"virtualApiKeysCreate": "가상 API 키 생성",
|
||||||
|
"virtualApiKeysCreateDescription": "이 조직의 공용 AI 게이트웨이를 호출할 수 있는 수동 키 발급",
|
||||||
|
"virtualApiKeysCreateButton": "키 생성",
|
||||||
|
"virtualApiKeysEmpty": "가상 API 키가 아직 없습니다",
|
||||||
|
"virtualApiKeysName": "이름",
|
||||||
|
"virtualApiKeysDescriptionOptional": "설명 (선택사항)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "사용자 연결 (선택사항)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "이 키를 사용자와 연결하여 사용을 추적합니다. 생성되면 선택한 공용 AI 게이트웨이에 즉시 액세스 권한이 부여됩니다. 수동으로 리소스와 연결할 필요가 없습니다. 키는 사용자의 프로필에도 표시됩니다.",
|
||||||
|
"virtualApiKeysAllResources": "모든 공용 AI 게이트웨이",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "이 키가 조직의 모든 공용 AI 게이트웨이에 액세스할 수 있도록 허용",
|
||||||
|
"virtualApiKeysSelectResources": "공용 AI 게이트웨이",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "리소스 선택",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "이 키가 액세스할 수 있는 공용 AI 게이트웨이를 선택하십시오",
|
||||||
|
"virtualApiKeysNoResources": "리소스 없음",
|
||||||
|
"virtualApiKeysSecret": "키",
|
||||||
|
"virtualApiKeysCopyKey": "이 키를 복사하십시오. 나중에 표에서 또는 편집할 때 다시 볼 수 있습니다.",
|
||||||
|
"virtualApiKeysViewSecret": "비밀 보기",
|
||||||
|
"virtualApiKeysViewSecretTitle": "가상 API 키 비밀",
|
||||||
|
"virtualApiKeysViewSecretDescription": "이 비밀은 이 키에 할당된 공용 AI 게이트웨이에 대한 액세스를 부여합니다",
|
||||||
|
"virtualApiKeysEdit": "가상 API 키 편집",
|
||||||
|
"virtualApiKeysEditDescription": "이 키의 관련 사용자 및 공용 AI 게이트웨이 액세스를 업데이트하십시오",
|
||||||
|
"virtualApiKeysSaveButton": "변경 사항 저장",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "최소한 하나의 공용 AI 게이트웨이를 선택하거나 모든 공용 AI 게이트웨이를 활성화하십시오",
|
||||||
|
"virtualApiKeysUpdated": "가상 API 키가 업데이트되었습니다",
|
||||||
|
"virtualApiKeysUpdatedDescription": "가상 API 키가 업데이트되었습니다",
|
||||||
|
"virtualApiKeysErrorUpdate": "가상 API 키 업데이트 오류",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "가상 API 키 업데이트에 실패했습니다",
|
||||||
|
"virtualApiKeysErrorCreate": "가상 API 키 생성 오류",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "가상 API 키 생성에 실패했습니다",
|
||||||
|
"virtualApiKeysErrorDelete": "가상 API 키 삭제 오류",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "가상 API 키 삭제에 실패했습니다",
|
||||||
|
"virtualApiKeysDeleted": "가상 API 키가 삭제되었습니다",
|
||||||
|
"virtualApiKeysDeletedDescription": "가상 API 키가 삭제되었습니다",
|
||||||
|
"virtualApiKeysDelete": "가상 API 키 삭제",
|
||||||
|
"virtualApiKeysDeleteConfirm": "키 삭제",
|
||||||
|
"virtualApiKeysQuestionRemove": "이 가상 API 키를 삭제하시겠습니까?",
|
||||||
|
"virtualApiKeysMessageRemove": "이 키를 사용하는 클라이언트는 즉시 액세스를 잃습니다.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "비밀 로딩 오류",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "가상 API 키 비밀 로딩에 실패했습니다",
|
||||||
|
"virtualApiKeysFilterUnassigned": "할당되지 않음",
|
||||||
|
"virtualApiKeysInferenceBudget": "예산",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "이 키가 지출 또는 토큰 제한에 따라 AI 사용을 제한하는 방법을 구성하십시오",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "생성 시 이메일 키",
|
||||||
|
"virtualApiKeysEmailThisKey": "이 키를 이메일로 발송",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "생성 후 이 키를 관련 사용자 및 추가 주소로 보냅니다",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "현재 키를 관련 사용자 및 추가 주소로 보냅니다",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "이 서버에 이메일 구성이 되어 있지 않습니다",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "가상 API 키를 이메일로 보내려면 SMTP를 구성하십시오.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "관련 사용자에게 전송",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "관련 사용자의 계정 이메일로 키 전송",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "키를 해당 사용자에게 보내기 위해 사용자를 연결하십시오",
|
||||||
|
"virtualApiKeysEmailAdditional": "추가 이메일",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "이메일을 추가하고 Enter를 누르십시오",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "관련 사용자를 선택하거나 최소한 하나의 이메일 주소를 추가하십시오",
|
||||||
|
"myVirtualApiKeysTitle": "귀하의 API 키",
|
||||||
|
"myVirtualApiKeysDescription": "이 조직에서 자신에게 할당된 신원 키 및 가상 API 키를 확인하십시오",
|
||||||
|
"myVirtualApiKeysResourceTitle": "{resourceName}에 대한 귀하의 API 키",
|
||||||
|
"myVirtualApiKeysResourceDescription": "{resourceName}에 액세스할 수 있도록 귀하에게 할당된 신원 키 및 가상 API 키를 확인하십시오",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "신원 키",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "개인 API 키",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "이 조직에 대한 귀하의 개인 키입니다. AI 게이트웨이 리소스를 호출할 때 귀하를 식별하는 데 사용됩니다.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "{resourceName}에 대한 귀하의 개인 API 키",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "이 조직에 대한 귀하의 개인 키입니다. {resourceName}을 호출하는 데 사용하십시오.",
|
||||||
|
"myVirtualApiKeysManualTitle": "귀속 키",
|
||||||
|
"myVirtualApiKeysManualDescription": "관리자가 귀하의 계정에 연결한 수동 가상 API 키",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "{resourceName}에 액세스할 수 있는 귀하의 계정에 연결된 수동 가상 API 키",
|
||||||
|
"myVirtualApiKeysManualEmpty": "아직 귀속된 키가 없습니다",
|
||||||
|
"myVirtualApiKeysKindUser": "신원",
|
||||||
|
"myVirtualApiKeysKindManual": "수동",
|
||||||
|
"myVirtualApiKeysUnnamed": "이름 없는 키",
|
||||||
|
"myVirtualApiKeysRevealSecret": "비밀 공개",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "이 비밀은 AI 게이트웨이 리소스에 대한 귀하의 인증을 수행합니다",
|
||||||
|
"aiProvidersTitle": "AI 공급자",
|
||||||
|
"aiProvidersDescription": "이 조직의 AI 작업량에 대한 모델 공급자를 연결하십시오",
|
||||||
|
"aiProvidersBannerTitle": "모델 공급자 연결",
|
||||||
|
"aiProvidersBannerDescription": "공급자는 AI 작업량을 위한 백엔드 모델입니다. 여기에서 OpenAI, Anthropic 및 기타 공급자를 연결한 다음 공용 AI 게이트웨이에 연결하여 사용자가 정체성 인식 액세스, 예산 및 로깅으로 모델을 호출할 수 있도록 하십시오.",
|
||||||
|
"aiProvidersAdd": "공급자 추가",
|
||||||
|
"aiProvidersSearch": "공급자 검색...",
|
||||||
|
"aiProvidersEmpty": "아직 AI 공급자가 없습니다",
|
||||||
|
"aiProviderCreate": "AI 공급자 생성",
|
||||||
|
"aiProviderCreateDescription": "이 조직을 위한 모델 공급자 추가",
|
||||||
|
"aiProviderSeeAll": "모든 공급자 보기",
|
||||||
|
"aiProviderSetting": "{providerName}을 위한 공급자 설정",
|
||||||
|
"aiProviderSettingDescription": "이 AI 공급자 구성을 설정하십시오",
|
||||||
|
"aiProviderGeneral": "일반",
|
||||||
|
"aiProviderGeneralDescription": "이 공급자의 기본 설정",
|
||||||
|
"aiProviderConfiguration": "설정",
|
||||||
|
"aiProviderConfigurationDescription": "이 공급자의 네트워크 라우팅 및 인증 설정",
|
||||||
|
"aiProviderNetworkSettings": "네트워크 설정",
|
||||||
|
"aiProviderNetworkSettingsDescription": "이 공급자로 트래픽을 전달하는 방법을 선택하십시오",
|
||||||
|
"aiProviderAuthSettings": "인증",
|
||||||
|
"aiProviderAuthSettingsDescription": "이 공급자가 업스트림 URL에 대한 요청을 인증하는 방법을 구성하십시오",
|
||||||
|
"aiProviderBudgetSettings": "예산",
|
||||||
|
"aiProviderBudgetSettingsDescription": "이 공급자가 지출 또는 토큰 한도에 따라 사용을 제한하는 방법을 구성하십시오",
|
||||||
|
"aiBudgetAdd": "예산 추가",
|
||||||
|
"aiBudgetEmpty": "아직 예산 구성되지 않았습니다. 예산 추가를 클릭하여 지출 또는 토큰 한도를 설정하십시오.",
|
||||||
|
"aiBudgetUnit": "지출 유형",
|
||||||
|
"aiBudgetPeriod": "재설정 기간",
|
||||||
|
"aiBudgetAmount": "최대 지출",
|
||||||
|
"aiBudgetAmountPlaceholder": "최대 지출",
|
||||||
|
"aiBudgetPeriodHourly": "시간당",
|
||||||
|
"aiBudgetPeriodDaily": "일일",
|
||||||
|
"aiBudgetPeriodWeekly": "주간",
|
||||||
|
"aiBudgetPeriodMonthly": "월간",
|
||||||
|
"aiBudgetPeriodYearly": "연간",
|
||||||
|
"aiBudgetPeriodLifetime": "평생",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "토큰",
|
||||||
|
"aiBudgetConflictError": "이미 이 재설정 기간과 지출 유형에 대한 예산이 존재합니다",
|
||||||
|
"aiBudgetInvalidAmountError": "0보다 큰 최대 지출을 입력하십시오",
|
||||||
|
"aiBudgetUpdated": "예산 업데이트됨",
|
||||||
|
"aiBudgetErrorSave": "예산 업데이트 실패",
|
||||||
|
"aiProviderType": "공급자 유형",
|
||||||
|
"aiProviderTypeSearch": "공급자 검색...",
|
||||||
|
"aiProviderTypeNotFound": "공급자 유형 없음",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "Custom",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "자체 엔드포인트를 가져오거나 사이트 타겟을 통해 라우트하십시오",
|
||||||
|
"aiProviderUpstreamUrl": "상류 URL",
|
||||||
|
"aiProviderUpstreamUrlDescription": "공급자 API의 기본 URL",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "기본 상류 URL 사용을 위해 빈칸으로 두십시오",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "유효 상류 URL",
|
||||||
|
"aiProviderApiKey": "API 키",
|
||||||
|
"aiProviderApiKeyDescription": "이 공급자에 대한 요청을 인증하기 위해 사용하는 API 키",
|
||||||
|
"aiProviderCustomHeadersDescription": "이 공급자에게 보내는 모든 요청에 대해 전송되는 헤더. 새 줄로 구분된: 헤더-이름: 값",
|
||||||
|
"aiProviderApiKeyLastChars": "API 키",
|
||||||
|
"aiProviderAuthType": "인증 유형",
|
||||||
|
"aiProviderAuthTypeSearch": "인증 유형 검색...",
|
||||||
|
"aiProviderAuthTypeNotFound": "인증 유형을 찾을 수 없습니다",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Authorization: Bearer 키. OpenAI 및 대부분의 공급자에서 사용",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key 헤더. Anthropic에서 사용",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key 헤더. Google Gemini에서 사용",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Authorization: Splunk 키. Splunk HTTP 이벤트 수집기에서 사용",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI 게이트웨이",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer 키. Cloudflare AI 게이트웨이에서 사용",
|
||||||
|
"aiProviderAuthTypeNone": "인증 없음",
|
||||||
|
"aiProviderAuthTypePassthrough": "통과",
|
||||||
|
"aiProviderAuthTypeDescription": "상류 API에서 요청을 인증하는 방법",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "발신자의 API 키 헤더를 상류로 전달",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "상류로 인증 헤더를 보내지 않음",
|
||||||
|
"aiProviderRoutingMode": "라우팅 모드",
|
||||||
|
"aiProviderRoutingModeDescription": "상류 URL로 트래픽을 보내거나 사이트의 HTTP 타겟으로 보냅니다",
|
||||||
|
"aiProviderRoutingModeUrl": "상류 URL",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "공개 또는 개인 API의 기본 URL 호출",
|
||||||
|
"aiProviderRoutingModeTarget": "사이트 타겟",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "사이트의 타겟을 통해 라우트",
|
||||||
|
"aiProviderRoutingModeTargetNote": "이 공급자를 생성한 후 네트워크 설정 탭에 사이트 타겟을 구성합니다.",
|
||||||
|
"aiProviderTargetNoOne": "이 공급자에게 타겟이 없습니다. 사이트를 통해 요청을 라우트하기 위한 타겟을 추가하십시오.",
|
||||||
|
"aiProviderSkipTlsVerification": "TLS 검증 건너뛰기",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "상류 연결에 대한 TLS 인증서 검증 비활성화",
|
||||||
|
"aiProviderBudget": "예산",
|
||||||
|
"aiProviderBudgetDescription": "이 공급자에 대한 선택적 지출 또는 토큰 예산",
|
||||||
|
"aiProviderBudgetAmount": "예산 금액",
|
||||||
|
"aiProviderBudgetUnit": "예산 단위",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "토큰",
|
||||||
|
"aiProviderEnabled": "활성화됨",
|
||||||
|
"aiProviderEnabledDescription": "모든 리소스에 대해 이 공급자를 완전히 비활성화합니다",
|
||||||
|
"aiProviderErrorCreate": "AI 공급자 생성에 실패했습니다",
|
||||||
|
"aiProviderErrorUpdate": "AI 공급자 업데이트에 실패했습니다",
|
||||||
|
"aiProviderErrorDelete": "AI 공급자 삭제에 실패했습니다",
|
||||||
|
"aiProviderErrorLoad": "AI 공급자 로드에 실패했습니다",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "유효한 상류 URL을 입력하십시오",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "이 공급자에는 상류 URL이 필요합니다",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "인증 유형이 필요합니다",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API 키가 필요합니다",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "사이트 타겟 라우팅은 맞춤형 공급자에게만 가능합니다",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "최소 한 가지 API 기능을 선택하십시오",
|
||||||
|
"aiProviderCapabilities": "API 기능",
|
||||||
|
"aiProviderCapabilitiesDescription": "이 공급자가 처리할 수 있는 API 형식을 선택하십시오. 알려진 공급자는 권장 기본값으로 시작합니다.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "사용자 지정 제공자가 처리할 수 있는 API 형식을 선택하십시오",
|
||||||
|
"aiProviderCapabilitiesSelect": "기능 선택",
|
||||||
|
"aiProviderCapabilitiesEmpty": "능력을 찾을 수 없습니다",
|
||||||
|
"aiProviderCapabilitiesSearch": "능력 검색...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI 채팅 완료",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "/v1/chat/completions 지원",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI 응답",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "/v1/responses 지원",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic 메시지",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "/v1/messages 지원",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini 콘텐츠 생성",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "직접 Gemini API 지원",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Bedrock 모델 실행",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Amazon Bedrock InvokeModel 지원",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex 콘텐츠 생성",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Vertex AI Gemini 형식 지원",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex Raw 예측",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Anthropic 모델에 대한 Vertex AI rawPredict 지원",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock 회화",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Amazon Bedrock Converse API 지원",
|
||||||
|
"aiProviderCreated": "AI 제공자가 생성되었습니다",
|
||||||
|
"aiProviderUpdated": "AI 제공자가 업데이트되었습니다",
|
||||||
|
"aiProviderDeleted": "AI 제공자가 삭제되었습니다",
|
||||||
|
"aiProviderDelete": "제공자 삭제",
|
||||||
|
"aiProviderDeleteConfirm": "제공자 삭제",
|
||||||
|
"aiProviderQuestionRemove": "이 AI 제공자를 삭제하시겠습니까?",
|
||||||
|
"aiProviderMessageRemove": "이 작업을 실행하면 제공자와 해당 모델 및 대상이 영구적으로 삭제됩니다. 이는 되돌릴 수 없습니다.",
|
||||||
|
"aiProviderErrorNoUpdate": "AI 제공자를 업데이트할 수 없습니다",
|
||||||
|
"aiProviderModels": "모델",
|
||||||
|
"aiProviderModelsDescription": "이 제공자에 대한 허용 및 차단 목록을 정의하십시오. 요청은 허용 항목과 일치해야 하며 차단 항목과 일치하지 않아야 합니다.",
|
||||||
|
"aiProviderCreateModelsDescription": "이 제공자가 제공할 수 있는 모델을 선택합니다. 허용 목록이 비어 있으면 모든 트래픽이 거부됩니다. 나중에 모델을 추가하거나 변경할 수 있습니다.",
|
||||||
|
"aiProviderModelsPlaceholder": "모델을 검색하거나 사용자 지정 키를 입력하세요",
|
||||||
|
"aiProviderModelsAllow": "허용 목록",
|
||||||
|
"aiProviderModelsAllowDescription": "이 제공자를 통해 사용할 수 있는 모델입니다. 비어 있으면 모두 거부를 의미합니다.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "모델 키 입력",
|
||||||
|
"aiProviderModelsAllowEmpty": "아직 추가된 모델이 없습니다.",
|
||||||
|
"aiProviderModelsBlock": "차단 목록",
|
||||||
|
"aiProviderModelsBlockDescription": "허용 항목과 일치하더라도 사용할 수 없는 모델입니다.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "모델 키 입력",
|
||||||
|
"aiProviderModelsBlockEmpty": "아직 추가된 모델이 없습니다.",
|
||||||
|
"aiProviderModelsAdd": "모델 추가",
|
||||||
|
"aiProviderModelsClearAll": "모두 지우기",
|
||||||
|
"aiProviderModelsAddCustom": "\"{key}\" 추가",
|
||||||
|
"aiProviderModelsAddCustomHint": "이 사용자 지정 모델 키를 추가하려면 Enter 키를 누르십시오.",
|
||||||
|
"aiProviderModelsAddBulk": "{count}개의 사용자 지정 키 추가",
|
||||||
|
"aiProviderModelsAddBulkHint": "{count}개의 사용자 지정 키를 추가하려면 Enter 키를 누르십시오.",
|
||||||
|
"aiProviderModelsAddOne": "지금 {key} 추가",
|
||||||
|
"aiProviderModelsAddSelected": "선택 항목 추가",
|
||||||
|
"aiProviderModelsSelectedCount": "{count}개 선택됨",
|
||||||
|
"aiProviderModelsSelectAll": "모두 선택",
|
||||||
|
"aiProviderModelsClearSelected": "지우기",
|
||||||
|
"aiProviderModelsBulkHint": "알려진 모델을 선택하거나 사용자 지정 키를 입력하십시오.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "일치하는 카탈로그 모델이 없습니다.",
|
||||||
|
"aiProviderModelsCatalogHeading": "알려진 모델",
|
||||||
|
"aiProviderModelsAllLabel": "모든 모델",
|
||||||
|
"aiProviderModelsAllPatternHint": "와일드카드: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "모든 모델 허용",
|
||||||
|
"aiProviderModelsAddAllBlock": "모델 모두 차단",
|
||||||
|
"aiProviderModelsAddAllDescription": "* 와일드카드를 사용하여 모든 모델 키를 일치시킵니다.",
|
||||||
|
"aiProviderModelsViewMore": "더 보기 ({count})",
|
||||||
|
"aiProviderModelsViewLess": "덜 보기",
|
||||||
|
"aiProviderModelsRemove": "모델 제거",
|
||||||
|
"aiProviderModelsEditHint": "모델 설정을 수정하려면 클릭하세요",
|
||||||
|
"aiProviderModelsSourceCatalog": "알려진 카탈로그 모델",
|
||||||
|
"aiProviderModelsSourceCustom": "사용자 지정 모델 키",
|
||||||
|
"aiProviderModelsSourcePattern": "와일드카드 패턴",
|
||||||
|
"aiProviderModelsSourceAll": "모든 모델 키와 일치",
|
||||||
|
"aiProviderModelsBudgetConfigured": "설정된 예산",
|
||||||
|
"aiProviderModelsEditTitle": "모델 편집",
|
||||||
|
"aiProviderModelsEditDescription": "모델 키를 업데이트하거나 해당 예산을 구성하십시오.",
|
||||||
|
"aiProviderModelsBudgetTab": "예산",
|
||||||
|
"aiProviderModelsKeyLabel": "모델 키",
|
||||||
|
"aiProviderModelsKeyRequired": "모델 키를 입력하십시오",
|
||||||
|
"aiProviderModelsKeyDuplicate": "이 모델 키는 이미 목록에 있습니다",
|
||||||
|
"aiProviderModelsOverlapError": "이 패턴은 두 목록에 모두 있을 수 없습니다: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "모델이 업데이트되었습니다",
|
||||||
|
"aiProviderModelsErrorUpdate": "모델 업데이트에 실패했습니다",
|
||||||
|
"aiResourceProviders": "제공자",
|
||||||
|
"aiResourceProvidersDescription": "이 AI 게이트웨이가 사용할 수 있는 AI 제공자를 선택합니다",
|
||||||
|
"aiResourceProvidersHelp": "공급자를 연결하고 상속(각 공급자의 목록 사용) 또는 선택(이 리소스에 대한 허용 목록 선택)을 선택하십시오. 연결된 공급자 전반에 걸쳐 충돌하는 허용 패턴은 허용되지 않습니다.",
|
||||||
|
"aiResourceProvidersSelect": "공급자 선택",
|
||||||
|
"aiResourceProvidersEmpty": "AI 제공자를 찾을 수 없습니다",
|
||||||
|
"aiResourceProvidersNoneAttached": "아직 연결된 공급자가 없습니다.",
|
||||||
|
"aiResourceProvidersAdd": "제공자 추가",
|
||||||
|
"aiResourceProvidersRemove": "제공자 제거",
|
||||||
|
"aiResourceProviderToggleEnabled": "이 리소스에서 공급자를 활성화하거나 비활성화합니다",
|
||||||
|
"aiResourceProviderDisabled": "사용 중지됨",
|
||||||
|
"aiResourceProvidersUpdated": "공급자가 업데이트되었습니다",
|
||||||
|
"aiResourceProvidersErrorUpdate": "공급자 업데이트에 실패했습니다",
|
||||||
|
"aiResourceProviderEditDescription": "이 공급자의 모델이 리소스에서 어떻게 노출되는지 선택하십시오.",
|
||||||
|
"viewProviderSettings": "제공자 설정 보기",
|
||||||
|
"aiResourceProviderMode": "액세스 모드",
|
||||||
|
"aiResourceProviderModeInherit": "상속",
|
||||||
|
"aiResourceProviderModeSelect": "선택",
|
||||||
|
"aiResourceProviderModeSelectSummary": "선택 · {count} 모델",
|
||||||
|
"aiResourceProviderModeInheritHelp": "공급자에서 구성한 대로 이 공급자의 허용 및 차단 목록을 사용하십시오.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "이 리소스를 위한 공급자의 허용 목록 모델의 하위 집합을 선택하십시오.",
|
||||||
|
"aiResourceProviderAllowModels": "허용 목록",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "모델 선택",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "모델 검색...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "모델을 찾을 수 없습니다",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "제공자의 허용 목록에 있는 모델만 선택할 수 있습니다.",
|
||||||
|
"aiResourceAliasRequired": "AI 게이트웨이에 별칭이 필요합니다",
|
||||||
|
"aiResourceDomainConfiguration": "도메인 구성",
|
||||||
|
"aiResourceDomainConfigurationDescription": "고객이 이 AI 게이트웨이를 사용하기 위해 사용할 도메인을 선택하십시오.",
|
||||||
|
"aiUsageAnalyticsTitle": "AI 사용 분석",
|
||||||
|
"aiUsageAnalyticsDescription": "제공자, 리소스, 역할 및 사용자 간의 AI 게이트웨이 비용, 토큰 사용량 및 활동을 분석합니다",
|
||||||
|
"aiUsageTabOverview": "개요",
|
||||||
|
"aiUsageTabProviders": "공급자",
|
||||||
|
"aiUsageTabResources": "리소스",
|
||||||
|
"aiUsageFilterProvider": "제공자",
|
||||||
|
"aiUsageFilterModel": "모델",
|
||||||
|
"aiUsageFilterResource": "리소스",
|
||||||
|
"aiUsageFilterRole": "역할",
|
||||||
|
"aiUsageFilterUser": "사용자",
|
||||||
|
"aiUsageFilterAllProviders": "모든 공급자",
|
||||||
|
"aiUsageFilterAllModels": "모든 모델",
|
||||||
|
"aiUsageFilterAllResources": "모든 리소스",
|
||||||
|
"aiUsageFilterAllRoles": "모든 역할",
|
||||||
|
"aiUsageFilterAllUsers": "모든 사용자",
|
||||||
|
"aiUsageFilterSearch": "검색...",
|
||||||
|
"aiUsageFilterNotFound": "옵션을 찾을 수 없습니다",
|
||||||
|
"aiUsageResetFilters": "필터 재설정",
|
||||||
|
"aiUsageRefresh": "새로 고침",
|
||||||
|
"aiUsageTokenTypePrompt": "프롬프트",
|
||||||
|
"aiUsageTokenTypeCacheRead": "캐시 읽기",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "캐시 쓰기",
|
||||||
|
"aiUsageTokenTypeCompletion": "완료",
|
||||||
|
"aiUsageTokenTypeReasoning": "추론",
|
||||||
|
"aiUsageRequests": "요청",
|
||||||
|
"aiUsageCost": "비용",
|
||||||
|
"aiUsageTokens": "토큰",
|
||||||
|
"aiUsageOther": "기타",
|
||||||
|
"aiUsageTotalRequests": "총 요청 수",
|
||||||
|
"aiUsageTotalTokens": "총 토큰수",
|
||||||
|
"aiUsageTotalCost": "총 비용",
|
||||||
|
"aiUsageEstimated": "추정치",
|
||||||
|
"aiUsageRequestVolume": "요청량",
|
||||||
|
"aiUsageTokenUsage": "토큰 사용",
|
||||||
|
"aiUsageModelCost": "모델 비용",
|
||||||
|
"aiUsageModelTokens": "모델 토큰",
|
||||||
|
"aiUsageTopModels": "상위 모델",
|
||||||
|
"aiUsageTopProviders": "상위 제공자",
|
||||||
|
"aiUsageProviderCost": "제공자 비용",
|
||||||
|
"aiUsageProviderTokenUsage": "제공자 토큰 사용량",
|
||||||
|
"aiUsageTopResources": "최고의 리소스",
|
||||||
|
"aiUsageResourceCost": "리소스 비용",
|
||||||
|
"aiUsageResourceTokenUsage": "리소스 토큰 사용량",
|
||||||
|
"aiUsageResourceTypePublic": "공용 리소스",
|
||||||
|
"aiUsageResourceTypeSite": "개인 리소스",
|
||||||
|
"aiUsageNoResource": "리소스 없음",
|
||||||
|
"aiUsageRolesTab": "역할",
|
||||||
|
"aiUsageUsersTab": "사용자들",
|
||||||
|
"aiUsageTopRoles": "최고의 역할",
|
||||||
|
"aiUsageRoleCost": "역할 비용",
|
||||||
|
"aiUsageRoleTokenUsage": "역할 토큰 사용량",
|
||||||
|
"aiUsageTopUsers": "최고의 사용자",
|
||||||
|
"aiUsageUserCost": "사용자 비용",
|
||||||
|
"aiUsageUserTokenUsage": "사용자 토큰 사용량",
|
||||||
|
"aiUsageUnknownUser": "알 수 없는 사용자",
|
||||||
|
"aiUsageVirtualApiKeysTab": "가상 API 키",
|
||||||
|
"aiUsageFilterVirtualApiKey": "가상 API 키",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "모든 가상 API 키",
|
||||||
|
"aiUsageTopVirtualApiKeys": "최고의 가상 API 키",
|
||||||
|
"aiUsageVirtualApiKeyCost": "가상 API 키 비용",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "가상 API 키 사용량",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "가상 API 키 없음",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "이름 없는 키",
|
||||||
|
"aiUsageLoading": "로딩 중...",
|
||||||
|
"aiUsageNoData": "데이터 없음",
|
||||||
|
"resourceBudgetSettings": "예산",
|
||||||
|
"resourceBudgetSettingsDescription": "이 AI 게이트웨이가 지출 또는 토큰 제한에 따라 사용을 제한하는 방법을 구성하십시오",
|
||||||
"sidebarApiKeys": "API 키",
|
"sidebarApiKeys": "API 키",
|
||||||
"sidebarProvisioning": "프로비저닝",
|
"sidebarProvisioning": "프로비저닝",
|
||||||
"sidebarSettings": "설정",
|
"sidebarSettings": "설정",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "서브넷",
|
"subnetPlaceholder": "서브넷",
|
||||||
"addressDescription": "클라이언트의 내부 주소. 조직의 서브넷 내에 있어야 합니다.",
|
"addressDescription": "클라이언트의 내부 주소. 조직의 서브넷 내에 있어야 합니다.",
|
||||||
"selectSites": "사이트 선택",
|
"selectSites": "사이트 선택",
|
||||||
|
"selectResources": "리소스 선택",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, other {# 자원}}",
|
||||||
"selectLabels": "레이블 선택",
|
"selectLabels": "레이블 선택",
|
||||||
"sitesDescription": "클라이언트는 선택한 사이트에 연결됩니다.",
|
"sitesDescription": "클라이언트는 선택한 사이트에 연결됩니다.",
|
||||||
"clientInstallOlm": "Olm 설치",
|
"clientInstallOlm": "Olm 설치",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "시간은 초 단위입니다",
|
"timeIsInSeconds": "시간은 초 단위입니다",
|
||||||
"requireDeviceApproval": "장치 승인 요구",
|
"requireDeviceApproval": "장치 승인 요구",
|
||||||
"requireDeviceApprovalDescription": "이 역할을 가진 사용자는 장치가 연결되기 전에 관리자의 승인이 필요합니다.",
|
"requireDeviceApprovalDescription": "이 역할을 가진 사용자는 장치가 연결되기 전에 관리자의 승인이 필요합니다.",
|
||||||
"sshSettings": "SSH 설정",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI 게이트웨이",
|
||||||
"sshAccess": "SSH 접속",
|
"sshAccess": "SSH 접속",
|
||||||
"rdpSettings": "RDP 설정",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "VNC 설정",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH 서버",
|
"sshServer": "SSH 서버",
|
||||||
"rdpServer": "RDP 서버",
|
"rdpServer": "RDP 서버",
|
||||||
"vncServer": "VNC 서버",
|
"vncServer": "VNC 서버",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI 게이트웨이",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "스킴",
|
"editInternalResourceDialogScheme": "스킴",
|
||||||
"editInternalResourceDialogEnableSsl": "TLS 활성화",
|
"editInternalResourceDialogEnableSsl": "TLS 활성화",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "ICMP 핑 허용",
|
"privateResourceAllowIcmpPing": "ICMP 핑 허용",
|
||||||
"privateResourceNetworkAccess": "네트워크 접근",
|
"privateResourceNetworkAccess": "네트워크 접근",
|
||||||
"privateResourceNetworkAccessDescription": "이 리소스에 대한 TCP/UDP 포트 접근과 ICMP 핑이 허용되는지 여부를 제어합니다.",
|
"privateResourceNetworkAccessDescription": "이 리소스에 대한 TCP/UDP 포트 접근과 ICMP 핑이 허용되는지 여부를 제어합니다.",
|
||||||
"hostSettings": "호스트 설정",
|
"hostSettings": "호스트",
|
||||||
"cidrSettings": "CIDR 설정",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "리소스 속성",
|
"createInternalResourceDialogResourceProperties": "리소스 속성",
|
||||||
"createInternalResourceDialogName": "이름",
|
"createInternalResourceDialogName": "이름",
|
||||||
"createInternalResourceDialogSite": "사이트",
|
"createInternalResourceDialogSite": "사이트",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI 게이트웨이",
|
||||||
"scheme": "스킴",
|
"scheme": "스킴",
|
||||||
"createInternalResourceDialogScheme": "스킴",
|
"createInternalResourceDialogScheme": "스킴",
|
||||||
"createInternalResourceDialogEnableSsl": "TLS 활성화",
|
"createInternalResourceDialogEnableSsl": "TLS 활성화",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "유효한 비밀번호",
|
"validPassword": "유효한 비밀번호",
|
||||||
"validEmail": "유효한 이메일",
|
"validEmail": "유효한 이메일",
|
||||||
"validSSO": "유효한 SSO",
|
"validSSO": "유효한 SSO",
|
||||||
|
"validVirtualAPIKey": "유효한 가상 API 키",
|
||||||
"view": "보기",
|
"view": "보기",
|
||||||
"configManaged": "구성 관리됨",
|
"configManaged": "구성 관리됨",
|
||||||
"connectedClient": "연결된 클라이언트",
|
"connectedClient": "연결된 클라이언트",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "로그 보관",
|
"logRetention": "로그 보관",
|
||||||
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
|
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
|
||||||
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
|
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
|
||||||
|
"aiSessionLogs": "AI 게이트웨이 세션 로그",
|
||||||
|
"aiSessionLogsDescription": "이 조직의 AI 게이트웨이 요청에 대한 프롬프트 및 응답 대본을 봅니다",
|
||||||
|
"sidebarLogsAi": "세션 로그",
|
||||||
|
"commandLogsAi": "세션 로그",
|
||||||
|
"sidebarLogsAiUsage": "사용 분석",
|
||||||
|
"commandLogsAiUsage": "사용 분석",
|
||||||
|
"provider": "제공자",
|
||||||
|
"capability": "능력",
|
||||||
|
"model": "모델",
|
||||||
|
"virtualApiKey": "가상 API 키",
|
||||||
|
"noVirtualApiKey": "가상 API 키가 없습니다",
|
||||||
|
"stream": "스트림",
|
||||||
|
"streaming": "스트리밍",
|
||||||
|
"nonStreaming": "비스트리밍",
|
||||||
|
"statusCode": "상태 코드",
|
||||||
|
"aiSessionId": "세션 ID",
|
||||||
|
"aiSessionRequest": "요청",
|
||||||
|
"aiSessionResponse": "응답",
|
||||||
|
"aiSessionNoData": "캡처된 데이터 없음",
|
||||||
|
"aiSessionCouldNotParse": "(원시, 대본을 구문 분석할 수 없음)",
|
||||||
|
"aiSessionLogTruncated": "이 세션은 저장소 전송되기 전에 잘린 상태일 수 있습니다.",
|
||||||
|
"aiSessionViewRaw": "원시 JSON 보기",
|
||||||
|
"aiSessionViewChat": "채팅 보기",
|
||||||
|
"cost": "비용",
|
||||||
|
"estimated": "추정된",
|
||||||
|
"tokenUsage": "토큰 사용량",
|
||||||
|
"promptTokens": "프롬프트 토큰",
|
||||||
|
"cacheReadTokens": "캐시 조회 토큰",
|
||||||
|
"cacheWriteTokens": "캐시 쓰기 토큰",
|
||||||
|
"completionTokens": "완료 토큰",
|
||||||
|
"reasoningTokens": "추론 토큰",
|
||||||
|
"totalTokens": "총 토큰수",
|
||||||
"requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기",
|
"requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기",
|
||||||
"logRetentionRequestLabel": "HTTP 요청 로그 보관",
|
"logRetentionRequestLabel": "HTTP 요청 로그 보관",
|
||||||
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
|
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "작업 로그를 얼마나 오래 보관할지",
|
"logRetentionActionDescription": "작업 로그를 얼마나 오래 보관할지",
|
||||||
"logRetentionConnectionLabel": "연결 로그 보유 기간",
|
"logRetentionConnectionLabel": "연결 로그 보유 기간",
|
||||||
"logRetentionConnectionDescription": "연결 로그를 얼마나 오래 보유할지",
|
"logRetentionConnectionDescription": "연결 로그를 얼마나 오래 보유할지",
|
||||||
|
"logRetentionAISessionsLabel": "AI 게이트웨이 세션 로그 보관",
|
||||||
|
"logRetentionAISessionsDescription": "AI 게이트웨이 프롬프트/응답 세션 로그를 보관할 기간",
|
||||||
"logRetentionDisabled": "비활성화됨",
|
"logRetentionDisabled": "비활성화됨",
|
||||||
"logRetention3Days": "3 일",
|
"logRetention3Days": "3 일",
|
||||||
"logRetention7Days": "7 일",
|
"logRetention7Days": "7 일",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "소스 주소",
|
"sourceAddress": "소스 주소",
|
||||||
"destinationAddress": "대상 주소",
|
"destinationAddress": "대상 주소",
|
||||||
"duration": "지속 시간",
|
"duration": "지속 시간",
|
||||||
"licenseRequiredToUse": "이 기능을 사용하려면 <enterpriseLicenseLink>엔터프라이즈 에디션</enterpriseLicenseLink> 라이선스가 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition 라이센스</enterpriseEditionLink>가 필요합니다. <bookADemoLink>데모 또는 평가판 예약</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>엔터프라이즈 에디션</enterpriseEditionLink>이 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition 라이센스</enterpriseEditionLink>가 필요합니다. <bookADemoLink>데모 또는 평가판 예약</bookADemoLink>",
|
||||||
"certResolver": "인증서 해결사",
|
"certResolver": "인증서 해결사",
|
||||||
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
|
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
|
||||||
"selectCertResolver": "인증서 해결사 선택",
|
"selectCertResolver": "인증서 해결사 선택",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "다중 사이트를 선택하면 높은 가용성을 위해 회복력 있는 라우팅 및 페일오버가 가능해집니다.",
|
"internalResourceFormMultiSiteRoutingHelp": "다중 사이트를 선택하면 높은 가용성을 위해 회복력 있는 라우팅 및 페일오버가 가능해집니다.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "자세히 알아보기",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "자세히 알아보기",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "특정 TCP/UDP 포트에 대한 접근을 제한하거나 모든 포트를 허용/차단하십시오.",
|
"editInternalResourceDialogPortRestrictionsDescription": "특정 TCP/UDP 포트에 대한 접근을 제한하거나 모든 포트를 허용/차단하십시오.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "HTTP 구성",
|
"createInternalResourceDialogHttpConfiguration": "도메인 구성",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "이 리소스에 HTTP 또는 HTTPS로 도달하기 위한 도메인을 선택하세요.",
|
"createInternalResourceDialogHttpConfigurationDescription": "이 리소스에 HTTP 또는 HTTPS로 도달하기 위한 도메인을 선택하세요.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "HTTP 구성",
|
"editInternalResourceDialogHttpConfiguration": "도메인 구성",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "이 리소스에 HTTP 또는 HTTPS로 도달하기 위한 도메인을 선택하세요.",
|
"editInternalResourceDialogHttpConfigurationDescription": "이 리소스에 HTTP 또는 HTTPS로 도달하기 위한 도메인을 선택하세요.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "레이블 없음",
|
"resourceLauncherUnlabeled": "레이블 없음",
|
||||||
|
"resourceLauncherAiGateway": "AI 게이트웨이",
|
||||||
"resourceLauncherNoSite": "사이트 없음",
|
"resourceLauncherNoSite": "사이트 없음",
|
||||||
|
"resourceLauncherAvailableModels": "사용 가능한 모델",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "이 AI 게이트웨이에서 사용할 수 있는 모델입니다.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "이 리소스에 사용할 수 있는 모델이 없습니다.",
|
||||||
|
"resourceLauncherAvailableModelsError": "사용 가능한 모델을 로드할 수 없습니다.",
|
||||||
|
"resourceLauncherApiKeys": "API 키",
|
||||||
|
"resourceLauncherApiKeysDescription": "신원 키나 속성 키를 사용하여 이 리소스에 인증하십시오.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "신원 키",
|
||||||
|
"resourceLauncherApiKeysManual": "속성 키",
|
||||||
|
"resourceLauncherApiKeysEmpty": "이 리소스에 사용할 수 있는 API 키가 없습니다.",
|
||||||
|
"resourceLauncherApiKeysError": "API 키를 로드할 수 없습니다.",
|
||||||
"resourceLauncherNoResourcesInGroup": "이 그룹에는 리소스가 없습니다",
|
"resourceLauncherNoResourcesInGroup": "이 그룹에는 리소스가 없습니다",
|
||||||
"resourceLauncherEmptyStateTitle": "사용 가능한 리소스 없음",
|
"resourceLauncherEmptyStateTitle": "사용 가능한 리소스 없음",
|
||||||
"resourceLauncherEmptyStateDescription": "아직 리소스에 대한 액세스 권한이 없습니다. 액세스를 요청하려면 관리자에게 문의하세요.",
|
"resourceLauncherEmptyStateDescription": "아직 리소스에 대한 액세스 권한이 없습니다. 액세스를 요청하려면 관리자에게 문의하세요.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "런처 뷰를 삭제할 수 없습니다. 다시 시도하세요.",
|
"resourceLauncherViewDeleteFailedDescription": "런처 뷰를 삭제할 수 없습니다. 다시 시도하세요.",
|
||||||
"memberPortalPrevious": "이전",
|
"memberPortalPrevious": "이전",
|
||||||
"memberPortalNext": "다음",
|
"memberPortalNext": "다음",
|
||||||
"httpSettings": "HTTP 설정",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "TCP 설정",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "UDP 설정",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "보안 연결 설정 중…",
|
"sshConnectingDescription": "보안 연결 설정 중…",
|
||||||
"sshConnecting": "연결 중…",
|
"sshConnecting": "연결 중…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Vennligst sjekk at du er logget inn som riktig bruker.",
|
"inviteLoginUser": "Vennligst sjekk at du er logget inn som riktig bruker.",
|
||||||
"inviteErrorNoUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til ikke er for en bruker som eksisterer.",
|
"inviteErrorNoUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til ikke er for en bruker som eksisterer.",
|
||||||
"inviteCreateUser": "Vennligst opprett en konto først.",
|
"inviteCreateUser": "Vennligst opprett en konto først.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Invitasjoner kan kun aksepteres av interne kontoer. Logg ut og logg inn med passordet ditt for denne e-posten.",
|
||||||
|
"inviteLoginInternalOnly": "Invitasjoner krever en intern konto med et passord. Opprett en konto eller logg inn med passordet ditt.",
|
||||||
"goHome": "Gå hjem",
|
"goHome": "Gå hjem",
|
||||||
"inviteLogInOtherUser": "Logg inn som en annen bruker",
|
"inviteLogInOtherUser": "Logg inn som en annen bruker",
|
||||||
"createAnAccount": "Lag konto",
|
"createAnAccount": "Lag konto",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Ingen land funnet.",
|
"noCountryFound": "Ingen land funnet.",
|
||||||
"siteSelectionDescription": "Dette området vil gi tilkobling til mål.",
|
"siteSelectionDescription": "Dette området vil gi tilkobling til mål.",
|
||||||
"resourceType": "Ressurstype",
|
"resourceType": "Ressurstype",
|
||||||
"resourceTypeDescription": "Dette kontrollerer ressursprotokollen og hvordan den vil vises i nettleseren. Dette kan ikke endres senere.",
|
"resourceTypeDescription": "Dette styrer ressursprotokollen og kan ikke endres senere.",
|
||||||
|
"resourceTypeSearch": "Søk i ressurstyper...",
|
||||||
|
"resourceTypeNotFound": "Ingen ressurstype funnet",
|
||||||
|
"resourceTypeHttpDescription": "Proxy nettverkstrafikk over HTTPS ved bruk av et domenenavn",
|
||||||
|
"resourceTypeInferenceDescription": "Ruter AI-forespørsler gjennom tilkoblede leverandører",
|
||||||
|
"resourceTypeSshDescription": "Tilgang til en SSH-server fra nettleseren",
|
||||||
|
"resourceTypeRdpDescription": "Tilgang til en fjernskrivebord fra nettleseren",
|
||||||
|
"resourceTypeVncDescription": "Tilgang til et VNC-skrivebord fra nettleseren",
|
||||||
|
"resourceTypeTcpDescription": "Proxy rå TCP-trafikk ved bruk av et portnummer",
|
||||||
|
"resourceTypeUdpDescription": "Proxy rå UDP-trafikk ved bruk av et portnummer",
|
||||||
|
"privateResourceTypeDescription": "Dette styrer hvordan klienter kommer til ressursen. Dette kan ikke endres senere.",
|
||||||
|
"privateResourceTypeHostDescription": "Eksponer en enkel vert på nettverksområdet for tilkoblede klienter",
|
||||||
|
"privateResourceTypeCidrDescription": "Eksponer et CIDR-område på nettverksområdet for tilkoblede klienter",
|
||||||
|
"privateResourceTypeHttpDescription": "Tilgang til en HTTP- eller HTTPS-tjeneste gjennom et domene",
|
||||||
|
"privateResourceTypeSshDescription": "Tilgang til en SSH-server fra tilkoblede klienter",
|
||||||
"resourceDomainDescription": "Ressursen vil bli servert på dette fullstendig kvalifiserte domenenavnet.",
|
"resourceDomainDescription": "Ressursen vil bli servert på dette fullstendig kvalifiserte domenenavnet.",
|
||||||
"resourceHTTPSSettings": "HTTPS-innstillinger",
|
"resourceHTTPSSettings": "HTTPS-innstillinger",
|
||||||
"resourceHTTPSSettingsDescription": "Konfigurer hvordan ressursen skal nås over HTTPS",
|
"resourceHTTPSSettingsDescription": "Konfigurer hvordan ressursen skal nås over HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Lagre innstillinger",
|
"authMethodsSave": "Lagre innstillinger",
|
||||||
"policyAuthStackTitle": "Autentisering",
|
"policyAuthStackTitle": "Autentisering",
|
||||||
"policyAuthStackDescription": "Kontroller hvilke autentiseringsmetoder som kreves for å få tilgang til denne ressursen",
|
"policyAuthStackDescription": "Kontroller hvilke autentiseringsmetoder som kreves for å få tilgang til denne ressursen",
|
||||||
|
"policyAuthInferenceStackDescription": "Velg hvilke brukere og roller som kan autentisere seg til denne AI-porten",
|
||||||
"policyAuthOrLogicTitle": "Flere autentiseringsmetoder aktive",
|
"policyAuthOrLogicTitle": "Flere autentiseringsmetoder aktive",
|
||||||
"policyAuthOrLogicBanner": "Besøkende kan autentisere ved bruk av en hvilken som helst av de aktive metodene nedenfor. De trenger ikke å fullføre alle.",
|
"policyAuthOrLogicBanner": "Besøkende kan autentisere ved bruk av en hvilken som helst av de aktive metodene nedenfor. De trenger ikke å fullføre alle.",
|
||||||
"policyAuthMethodActive": "Aktiv",
|
"policyAuthMethodActive": "Aktiv",
|
||||||
"policyAuthMethodOff": "Av",
|
"policyAuthMethodOff": "Av",
|
||||||
"policyAuthSsoTitle": "Plattform SSO",
|
"policyAuthSsoTitle": "Plattform SSO",
|
||||||
"policyAuthSsoDescription": "Krev pålogging gjennom din organisasjons identitetsleverandør",
|
"policyAuthSsoDescription": "Krev pålogging gjennom din organisasjons identitetsleverandør",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "Logg inn URL",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Hver bruker har allerede en identitet API-nøkkel, så du trenger bare å opprette virtuelle API-nøkler for ikke-brukerklienter eller delt tilgang. Brukere kan hente sin nøkkel ved å logge inn med sin identitetsleverandør på denne ressursens URL, hvor den vil vises etter innlogging.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Hver bruker har allerede en identitet API-nøkkel, så du trenger bare å opprette virtuelle API-nøkler for ikke-brukerklienter eller delt tilgang. Brukere henter sin nøkkel her etter innlogging med sin identitetsleverandør.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} brukere, {roles} roller",
|
"policyAuthSsoSummary": "{idp} · {users} brukere, {roles} roller",
|
||||||
"policyAuthSsoDefaultIdp": "Standardleverandør",
|
"policyAuthSsoDefaultIdp": "Standardleverandør",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Legg til standard identitetsleverandør",
|
"policyAuthAddDefaultIdentityProvider": "Legg til standard identitetsleverandør",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Ny rolle kreves",
|
"accessRoleErrorNewRequired": "Ny rolle kreves",
|
||||||
"accessRoleErrorRemove": "Kunne ikke fjerne rolle",
|
"accessRoleErrorRemove": "Kunne ikke fjerne rolle",
|
||||||
"accessRoleErrorRemoveDescription": "Det oppstod en feil under fjerning av rollen.",
|
"accessRoleErrorRemoveDescription": "Det oppstod en feil under fjerning av rollen.",
|
||||||
|
"accessRoleInferenceBudget": "AI Budsjett",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Konfigurer hvordan medlemmene av denne rollen begrenser AI-bruk basert på forbruk eller tokenbegrensninger",
|
||||||
"accessRoleName": "Rollenavn",
|
"accessRoleName": "Rollenavn",
|
||||||
"accessRoleQuestionRemove": "Du er ferd med å slette rollen `{name}. Du kan ikke angre denne handlingen.",
|
"accessRoleQuestionRemove": "Du er ferd med å slette rollen `{name}. Du kan ikke angre denne handlingen.",
|
||||||
"accessRoleRemove": "Fjern Rolle",
|
"accessRoleRemove": "Fjern Rolle",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Slett område",
|
"actionDeleteSite": "Slett område",
|
||||||
"actionGetSite": "Hent område",
|
"actionGetSite": "Hent område",
|
||||||
"actionListSites": "List opp områder",
|
"actionListSites": "List opp områder",
|
||||||
|
"actionCreateAiProvider": "Opprett AI-leverandør",
|
||||||
|
"actionDeleteAiProvider": "Slett AI-leverandør",
|
||||||
|
"actionGetAiProvider": "Hent AI-leverandør",
|
||||||
|
"actionListAiProviders": "List opp AI-leverandører",
|
||||||
|
"actionUpdateAiProvider": "Oppdater AI-leverandør",
|
||||||
|
"actionCreateAiModel": "Opprett AI-modell",
|
||||||
|
"actionDeleteAiModel": "Slett AI-modell",
|
||||||
|
"actionGetAiModel": "Hent AI-modell",
|
||||||
|
"actionListAiModels": "List opp AI-modeller",
|
||||||
|
"actionUpdateAiModel": "Oppdater AI-modell",
|
||||||
|
"actionCreateAiBudget": "Opprett AI-budsjett",
|
||||||
|
"actionDeleteAiBudget": "Slett AI-budsjett",
|
||||||
|
"actionGetAiBudget": "Hent AI-budsjett",
|
||||||
|
"actionListAiBudgets": "List opp AI-budsjetter",
|
||||||
|
"actionUpdateAiBudget": "Oppdater AI-budsjett",
|
||||||
|
"actionListResourceAiModels": "List opp ressurs AI-modeller",
|
||||||
|
"actionSetResourceAiModels": "Angi ressurs AI-modeller",
|
||||||
|
"actionCreateVirtualApiKey": "Opprett virtuell API-nøkkel",
|
||||||
|
"actionDeleteVirtualApiKey": "Slett virtuell API-nøkkel",
|
||||||
|
"actionGetVirtualApiKey": "Hent virtuell API-nøkkel",
|
||||||
|
"actionListVirtualApiKeys": "List opp virtuelle API-nøkler",
|
||||||
|
"actionUpdateVirtualApiKey": "Oppdater virtuell API-nøkkel",
|
||||||
"actionApplyBlueprint": "Bruk blåkopi",
|
"actionApplyBlueprint": "Bruk blåkopi",
|
||||||
"actionListBlueprints": "List opp blåkopier",
|
"actionListBlueprints": "List opp blåkopier",
|
||||||
"actionGetBlueprint": "Hent blåkopi",
|
"actionGetBlueprint": "Hent blåkopi",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Angi ressurspinkode",
|
"actionSetResourcePincode": "Angi ressurspinkode",
|
||||||
"actionSetResourceEmailWhitelist": "Angi e-post-hviteliste for ressurs",
|
"actionSetResourceEmailWhitelist": "Angi e-post-hviteliste for ressurs",
|
||||||
"actionGetResourceEmailWhitelist": "Hent e-post-hviteliste for ressurs",
|
"actionGetResourceEmailWhitelist": "Hent e-post-hviteliste for ressurs",
|
||||||
|
"actionListResourcePolicies": "List opp ressursregler",
|
||||||
|
"actionCreateResourcePolicy": "Opprett ressursregel",
|
||||||
"actionGetResourcePolicy": "Hent ressursregel",
|
"actionGetResourcePolicy": "Hent ressursregel",
|
||||||
"actionUpdateResourcePolicy": "Oppdater ressursregel",
|
"actionUpdateResourcePolicy": "Oppdater ressursregel",
|
||||||
|
"actionDeleteResourcePolicy": "Slett ressursregel",
|
||||||
"actionSetResourcePolicyUsers": "Sett ressursregel for brukere",
|
"actionSetResourcePolicyUsers": "Sett ressursregel for brukere",
|
||||||
"actionSetResourcePolicyRoles": "Sett ressursregel for roller",
|
"actionSetResourcePolicyRoles": "Sett ressursregel for roller",
|
||||||
"actionSetResourcePolicyPassword": "Sett ressursregel for passord",
|
"actionSetResourcePolicyPassword": "Sett ressursregel for passord",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Invitasjoner",
|
"sidebarInvitations": "Invitasjoner",
|
||||||
"sidebarRoles": "Roller",
|
"sidebarRoles": "Roller",
|
||||||
"sidebarShareableLinks": "Delbare lenker",
|
"sidebarShareableLinks": "Delbare lenker",
|
||||||
|
"sidebarAiGateway": "AI Gateway",
|
||||||
|
"sidebarAiProviders": "Leverandører",
|
||||||
|
"commandAiProviders": "AI-leverandører",
|
||||||
|
"sidebarVirtualApiKeys": "Virtuelle API-nøkler",
|
||||||
|
"sidebarMyApiKeys": "Dine API-nøkler",
|
||||||
|
"sidebarAccount": "Starter",
|
||||||
|
"commandVirtualApiKeys": "Virtuelle API-nøkler",
|
||||||
|
"virtualApiKeysTitle": "Administrer virtuelle API-nøkler",
|
||||||
|
"virtualApiKeysDescription": "Opprett og administrer manuelle API-nøkler for AI Gateway-tilgang til offentlige AI-portene",
|
||||||
|
"virtualApiKeysTabIdentity": "Identitetsnøkler",
|
||||||
|
"virtualApiKeysTabVirtual": "Virtuelle nøkler",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Identitetsnøkler for hver bruker",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Hver bruker har allerede en Pangolin identitetsnøkkel for denne organisasjonen. Den er unik for brukerkontoen deres og autentiserer dem til AI-portene de kan få tilgang til.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Eksempel",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Hvordan brukere henter sin nøkkel",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Besøk en offentlig AI Gateway-URL i nettleseren og logg inn med kontoen din.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Eller gå til <url></url> mens du er logget inn.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Du kan opprette flere virtuelle API-nøkler i kategorien Virtuelle nøkler. Disse nøklene kan avgrenses til spesifikke offentlige AI-portene og eventuelt assosieres med en bruker. Opprettelse av en nøkkel gir umiddelbar tilgang til de valgte offentlige AI-portene.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Manuelt opprett en nøkkel",
|
||||||
|
"virtualApiKeysEmailIdentity": "E-postidentitetsnøkler",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Send hver valgt bruker eller rolle deres Pangolin identitetsnøkkel.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Send til alle brukere",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "E-post til alle medlemmer av organisasjonen som har en kontoe-post.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Brukere",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Roller",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Send e-poster",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Identitetsnøkler sendt på e-post",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Sendt {sent} e-poster.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} brukere ble hoppet over fordi de ikke har en e-postadresse.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Velg minst én bruker eller rolle, eller send til alle brukere.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Feil ved sending av identitetsnøkler",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Kunne ikke sende identitetsnøkler",
|
||||||
|
"virtualApiKeysBannerTitle": "Identitetsnøkler for hver bruker",
|
||||||
|
"virtualApiKeysBannerDescription": "Hver bruker har allerede en identitetsnøkkel tilgjengelig på {keysUrl}. Du kan også manuelt opprette nøkler her som gir direkte tilgang til offentlige AI-portene.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Vis identitetsnøkler",
|
||||||
|
"virtualApiKeys": "Virtuelle API-nøkler",
|
||||||
|
"virtualApiKeysSearch": "Søk nøklene...",
|
||||||
|
"virtualApiKeysCreate": "Opprett virtuell API-nøkkel",
|
||||||
|
"virtualApiKeysCreateDescription": "Lag en manuell nøkkel som kan kalle offentlige AI-gatewayer i denne organisasjonen",
|
||||||
|
"virtualApiKeysCreateButton": "Opprett nøkkel",
|
||||||
|
"virtualApiKeysEmpty": "Ingen virtuelle API-nøkler ennå",
|
||||||
|
"virtualApiKeysName": "Navn",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Beskrivelse (valgfritt)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Assosier bruker (valgfritt)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Assosier denne nøkkelen med en bruker for å spore bruken. Når den er opprettet, gir denne nøkkelen umiddelbar tilgang til de valgte offentlige AI-portene. Du trenger ikke å manuelt assosiere en bruker til ressursen. Nøkkelen vil også vises i brukerens profil.",
|
||||||
|
"virtualApiKeysAllResources": "Alle offentlige AI-portene",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Tillat denne nøkkelen å få tilgang til hver offentlig AI-port i organisasjonen",
|
||||||
|
"virtualApiKeysSelectResources": "Offentlige AI-portene",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Velg ressurser",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Velg hvilke offentlige AI-portene denne nøkkelen kan få tilgang til",
|
||||||
|
"virtualApiKeysNoResources": "Ingen ressurser",
|
||||||
|
"virtualApiKeysSecret": "Nøkkel",
|
||||||
|
"virtualApiKeysCopyKey": "Kopier denne nøkkelen. Du kan se den igjen senere fra tabellen eller når du redigerer.",
|
||||||
|
"virtualApiKeysViewSecret": "Vis hemmelighet",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Virtuell API-nøkkel hemmelighet",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Denne hemmeligheten gir tilgang til de offentlige AI-portene som er tildelt denne nøkkelen",
|
||||||
|
"virtualApiKeysEdit": "Rediger virtuell API-nøkkel",
|
||||||
|
"virtualApiKeysEditDescription": "Oppdater den tilknyttede brukeren og tilgang til offentlige AI-port for denne nøkkelen",
|
||||||
|
"virtualApiKeysSaveButton": "Lagre endringer",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Velg minst én offentlig AI-port, eller aktiver alle offentlige AI-portene",
|
||||||
|
"virtualApiKeysUpdated": "Virtuell API-nøkkel oppdatert",
|
||||||
|
"virtualApiKeysUpdatedDescription": "Den virtuelle API-nøkkelen er oppdatert",
|
||||||
|
"virtualApiKeysErrorUpdate": "Feil ved oppdatering av virtuell API-nøkkel",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Kunne ikke oppdatere virtuell API-nøkkel",
|
||||||
|
"virtualApiKeysErrorCreate": "Feil ved opprettelse av virtuell API-nøkkel",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Kunne ikke opprette virtuell API-nøkkel",
|
||||||
|
"virtualApiKeysErrorDelete": "Feil ved sletting av virtuell API-nøkkel",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Kunne ikke slette virtuell API-nøkkel",
|
||||||
|
"virtualApiKeysDeleted": "Virtuell API-nøkkel slettet",
|
||||||
|
"virtualApiKeysDeletedDescription": "Den virtuelle API-nøkkelen er slettet",
|
||||||
|
"virtualApiKeysDelete": "Slett virtuell API-nøkkel",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Slett nøkkel",
|
||||||
|
"virtualApiKeysQuestionRemove": "Er du sikker på at du vil slette denne virtuelle API-nøkkelen?",
|
||||||
|
"virtualApiKeysMessageRemove": "Klienter som bruker denne nøkkelen vil miste tilgang umiddelbart.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Feil ved lasting av hemmelighet",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Kunne ikke laste den virtuelle API-nøkkelen hemmelighet",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Uten hentydning",
|
||||||
|
"virtualApiKeysInferenceBudget": "Budsjett",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Konfigurer hvordan denne nøkkelen begrenser AI-bruk basert på forbruk eller token begrensninger",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "E-postnøkkel ved generering",
|
||||||
|
"virtualApiKeysEmailThisKey": "E-post denne nøkkelen",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Send nøkkelen til den tilknyttede brukeren og ytterligere adresser etter at den er opprettet",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Send den nåværende nøkkelen til den tilknyttede brukeren og ytterligere adresser",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "E-post er ikke konfigurert på denne serveren",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Konfigurér SMTP for å sende virtuelle API-nøkler.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Send til tilknyttet bruker",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Send nøkkelen til den tilknyttede brukerens kontoe-post",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Assosier en bruker for å sende nøkkelen til den brukeren",
|
||||||
|
"virtualApiKeysEmailAdditional": "Ekstra e-poster",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Legg til e-post og trykk Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Velg den tilknyttede brukeren eller legg til minst én e-postadresse",
|
||||||
|
"myVirtualApiKeysTitle": "Dine API-nøkler",
|
||||||
|
"myVirtualApiKeysDescription": "Vis din identitetsnøkkel og eventuelle virtuelle API-nøkler tilskrives deg i denne organisasjonen",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Dine API-nøkler for {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Vis din identitetsnøkkel og virtuelle API-nøkler tilskrives deg som kan få tilgang til {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Identitetsnøkkel",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Din personlige API-nøkkel",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Din personlige nøkkel for denne organisasjonen. Den er unik til kontoen din og brukes til å identifisere deg når du kaller AI Gateway-ressurser.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Din personlige API-nøkkel for {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Din personlige nøkkel for denne organisasjonen. Bruk den til å kalle {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Tilskrevne nøkler",
|
||||||
|
"myVirtualApiKeysManualDescription": "Manuelle virtuelle API-nøkler en admin assosiert med kontoen din",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Manuelle virtuelle API-nøkler assosiert med kontoen din som kan få tilgang til {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Ingen tilskrevne nøkler ennå",
|
||||||
|
"myVirtualApiKeysKindUser": "Identitet",
|
||||||
|
"myVirtualApiKeysKindManual": "Manuell",
|
||||||
|
"myVirtualApiKeysUnnamed": "Uten navngitt nøkkel",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Avslør hemmelighet",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Denne hemmeligheten autentiserer deg til AI Gateway-ressurser",
|
||||||
|
"aiProvidersTitle": "AI-leverandører",
|
||||||
|
"aiProvidersDescription": "Koble modellleverandører for AI-arbeidslaster i denne organisasjonen",
|
||||||
|
"aiProvidersBannerTitle": "Koble modellleverandører",
|
||||||
|
"aiProvidersBannerDescription": "Leverandører er modell-backendene Pangolin bruker for AI-arbeidslaster. Koble OpenAI, Anthropic, og andre leverandører her, og fest dem deretter til offentlige AI-portene slik at brukere kan kalle modeller med identitetsbevisst tilgang, budsjetter, og logging.",
|
||||||
|
"aiProvidersAdd": "Legg til leverandør",
|
||||||
|
"aiProvidersSearch": "Søk leverandører...",
|
||||||
|
"aiProvidersEmpty": "Ingen AI-leverandører ennå",
|
||||||
|
"aiProviderCreate": "Opprett AI-leverandør",
|
||||||
|
"aiProviderCreateDescription": "Legg til en modellleverandør for denne organisasjonen",
|
||||||
|
"aiProviderSeeAll": "Se alle leverandører",
|
||||||
|
"aiProviderSetting": "Leverandørinnstillinger for {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Konfigurer denne AI-leverandøren",
|
||||||
|
"aiProviderGeneral": "Generelt",
|
||||||
|
"aiProviderGeneralDescription": "Grunnleggende innstillinger for denne leverandøren",
|
||||||
|
"aiProviderConfiguration": "Konfigurasjon",
|
||||||
|
"aiProviderConfigurationDescription": "Nettverksrutting og autentisering for denne leverandøren",
|
||||||
|
"aiProviderNetworkSettings": "Nettverksinnstillinger",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Velg hvordan trafikken når denne leverandøren",
|
||||||
|
"aiProviderAuthSettings": "Autentisering",
|
||||||
|
"aiProviderAuthSettingsDescription": "Konfigurer hvordan denne leverandøren autentiserer forespørsler til sin oppstrøms-URL",
|
||||||
|
"aiProviderBudgetSettings": "Budsjett",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Konfigurer hvordan denne leverandøren begrenser bruk basert på forbruk eller tokenbegrensninger",
|
||||||
|
"aiBudgetAdd": "Legg til budsjett",
|
||||||
|
"aiBudgetEmpty": "Ingen budsjetter konfigurert ennå. Klikk Legg til budsjett for å angi en forbruks- eller tokenbegrensning.",
|
||||||
|
"aiBudgetUnit": "Brukstype",
|
||||||
|
"aiBudgetPeriod": "Tilbakestill periode",
|
||||||
|
"aiBudgetAmount": "Maksimal forbruk",
|
||||||
|
"aiBudgetAmountPlaceholder": "Maksimal forbruk",
|
||||||
|
"aiBudgetPeriodHourly": "Timebasis",
|
||||||
|
"aiBudgetPeriodDaily": "Daglig",
|
||||||
|
"aiBudgetPeriodWeekly": "Ukentlig",
|
||||||
|
"aiBudgetPeriodMonthly": "Månedlig",
|
||||||
|
"aiBudgetPeriodYearly": "Årlig",
|
||||||
|
"aiBudgetPeriodLifetime": "Livstid",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Tokens",
|
||||||
|
"aiBudgetConflictError": "Et budsjett for denne tilbakestillingsperioden og forbrukstype eksisterer allerede",
|
||||||
|
"aiBudgetInvalidAmountError": "Angi et maksimalt forbruk større enn 0",
|
||||||
|
"aiBudgetUpdated": "Budsjetter oppdatert",
|
||||||
|
"aiBudgetErrorSave": "Kunne ikke oppdatere budsjettene",
|
||||||
|
"aiProviderType": "Leverandørtype",
|
||||||
|
"aiProviderTypeSearch": "Søk leverandører...",
|
||||||
|
"aiProviderTypeNotFound": "Ingen leverandør type funnet",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "Tilpasset",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "Bruk din egen endepunkt eller rute via områdetargets",
|
||||||
|
"aiProviderUpstreamUrl": "Oppstrøms URL",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Basis URL for leverandørens API",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "La stå tom for å bruke standard oppstrøms URL for denne leverandøren",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Effektiv oppstrøms URL",
|
||||||
|
"aiProviderApiKey": "API-nøkkel",
|
||||||
|
"aiProviderApiKeyDescription": "API-nøkkel brukt for å autentisere forespørsler til denne leverandøren",
|
||||||
|
"aiProviderCustomHeadersDescription": "Overskrifter sendt ved hver forespørsel til denne leverandøren. Ny linje separert: Overskriftsnavn: verdi",
|
||||||
|
"aiProviderApiKeyLastChars": "API-nøkkel",
|
||||||
|
"aiProviderAuthType": "Autentiseringstype",
|
||||||
|
"aiProviderAuthTypeSearch": "Søk autentiseringstyper...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Ingen autentiseringstype funnet",
|
||||||
|
"aiProviderAuthTypeBearer": "Bærer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Autorisasjon: Bærer nøkkel. Brukt av OpenAI og de fleste leverandører",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key overskrift. Brukt av Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key overskrift. Brukt av Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Autorisasjon: Splunk nøkkel. Brukt av Splunk HTTP Event Collector",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bærer nøkkel. Brukt av Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "Ingen autentisering",
|
||||||
|
"aiProviderAuthTypePassthrough": "Gjennomkjøring",
|
||||||
|
"aiProviderAuthTypeDescription": "Hvordan det oppstrøms API-et autentiserer forespørsler",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Videreformidle anroperens API-nøkler til oppstrøms",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Ikke send autentiseringsoverskrifter til oppstrøms",
|
||||||
|
"aiProviderRoutingMode": "Ruteringsmodus",
|
||||||
|
"aiProviderRoutingModeDescription": "Send trafikk til en oppstrøms URL eller til HTTP-mål på dine nettsteder",
|
||||||
|
"aiProviderRoutingModeUrl": "Oppstrøms URL",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Ring en offentlig eller privat API-basis URL",
|
||||||
|
"aiProviderRoutingModeTarget": "Områdemål",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Rute gjennom mål på dine nettsteder",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Etter å ha opprettet denne leverandøren, konfigurer områdemål på fanen Nettverksinnstillinger.",
|
||||||
|
"aiProviderTargetNoOne": "Denne leverandøren har ingen mål. Legg til et mål for å rute forespørsler gjennom dine nettsteder.",
|
||||||
|
"aiProviderSkipTlsVerification": "Hopp over TLS-verifisering",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Deaktiver TLS-sertifikatverifisering for oppstrøms tilkobling",
|
||||||
|
"aiProviderBudget": "Budsjett",
|
||||||
|
"aiProviderBudgetDescription": "Valgfritt utgifts- eller tokenbudsjett for denne leverandøren",
|
||||||
|
"aiProviderBudgetAmount": "Budsjettbeløp",
|
||||||
|
"aiProviderBudgetUnit": "Budsjettenhet",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Tokens",
|
||||||
|
"aiProviderEnabled": "Aktivert",
|
||||||
|
"aiProviderEnabledDescription": "Deaktiver denne leverandøren fullstendig på tvers av alle ressurser",
|
||||||
|
"aiProviderErrorCreate": "Kunne ikke opprette AI-leverandør",
|
||||||
|
"aiProviderErrorUpdate": "Mislyktes å oppdatere AI-leverandør",
|
||||||
|
"aiProviderErrorDelete": "Kunne ikke slette AI-leverandør",
|
||||||
|
"aiProviderErrorLoad": "Kunne ikke laste AI-leverandør",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Skriv inn en gyldig oppstrøms URL",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Oppstrøms URL er nødvendig for denne leverandøren",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Autentiseringstype er påkrevd",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API-nøkkel er påkrevd",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Områdetargets rute er kun tilgjengelig for kundespesifikke leverandører",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Velg minst en API-kapasitet",
|
||||||
|
"aiProviderCapabilities": "API Kapasiteter",
|
||||||
|
"aiProviderCapabilitiesDescription": "Velg hvilke API-formater denne leverandøren kan håndtere. Kjente leverandører starter med anbefalte standarder.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Velg hvilke API-formater denne tilpassede leverandøren kan håndtere",
|
||||||
|
"aiProviderCapabilitiesSelect": "Velg kapasiteter",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Ingen kapasiteter funnet",
|
||||||
|
"aiProviderCapabilitiesSearch": "Søk kapasiteter...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Chat Fullføringer",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Støtter /v1/chat/fullføringer",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI Responser",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Støtter /v1/responser",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic Meldinger",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Støtter /v1/meldinger",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini Generer Innhold",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Støtter direkte Gemini API",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Bedrock Modell Påkalling",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Støtter Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex Generer Innhold",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Støtter Vertex AI Gemini format",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex Rå Prediksjon",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Støtter Vertex AI råPredikt for Anthropic-modeller",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock Samtale",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Støtter Amazon Bedrock Converse API",
|
||||||
|
"aiProviderCreated": "AI-leverandør opprettet",
|
||||||
|
"aiProviderUpdated": "AI-leverandør oppdatert",
|
||||||
|
"aiProviderDeleted": "AI-leverandør slettet",
|
||||||
|
"aiProviderDelete": "Slett Leverandør",
|
||||||
|
"aiProviderDeleteConfirm": "Slett Leverandør",
|
||||||
|
"aiProviderQuestionRemove": "Er du sikker på at du vil slette denne AI-leverandøren?",
|
||||||
|
"aiProviderMessageRemove": "Dette vil permanent slette leverandøren og dens modeller og mål. Dette kan ikke angres.",
|
||||||
|
"aiProviderErrorNoUpdate": "AI-leverandør er ikke tilgjengelig for oppdatering",
|
||||||
|
"aiProviderModels": "Modeller",
|
||||||
|
"aiProviderModelsDescription": "Definer tillat- og blokk-lister for denne leverandøren. Forespørslene må matche en tillat oppføring og må ikke samsvare med en blokk oppføring.",
|
||||||
|
"aiProviderCreateModelsDescription": "Velg hvilke modeller denne leverandøren kan betjene. En tom tillatsliste avviser alle trafikk. Du kan legge til eller endre modeller senere.",
|
||||||
|
"aiProviderModelsPlaceholder": "Søk modeller eller skriv inn en tilpasset nøkkel",
|
||||||
|
"aiProviderModelsAllow": "Tillat Liste",
|
||||||
|
"aiProviderModelsAllowDescription": "Modeller som kan brukes gjennom denne leverandøren. Tom betyr å nekte alt.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Skriv inn modellenøkkel",
|
||||||
|
"aiProviderModelsAllowEmpty": "Ingen modeller lagt til enda.",
|
||||||
|
"aiProviderModelsBlock": "Blokker Liste",
|
||||||
|
"aiProviderModelsBlockDescription": "Modeller for å nekte, selv om de samsvarer med en tillat oppføring.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Skriv inn modellenøkkel",
|
||||||
|
"aiProviderModelsBlockEmpty": "Ingen modeller lagt til enda.",
|
||||||
|
"aiProviderModelsAdd": "Legg til Modeller",
|
||||||
|
"aiProviderModelsClearAll": "Fjern alle",
|
||||||
|
"aiProviderModelsAddCustom": "Legg til \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Trykk Enter for å legge til denne tilpassede modellenøkkelen.",
|
||||||
|
"aiProviderModelsAddBulk": "Legg til {count} tilpassede nøkler",
|
||||||
|
"aiProviderModelsAddBulkHint": "Trykk Enter for å legge til {count} tilpassede nøkler.",
|
||||||
|
"aiProviderModelsAddOne": "Legg til {key} nå",
|
||||||
|
"aiProviderModelsAddSelected": "Legg til Valgt",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} valgt",
|
||||||
|
"aiProviderModelsSelectAll": "Velg alle",
|
||||||
|
"aiProviderModelsClearSelected": "Fjern",
|
||||||
|
"aiProviderModelsBulkHint": "Velg kjente modeller eller skriv inn en tilpasset nøkkel.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Ingen matchende katalogmodeller.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Kjente Modeller",
|
||||||
|
"aiProviderModelsAllLabel": "Alle modeller",
|
||||||
|
"aiProviderModelsAllPatternHint": "Jokertegn: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Tillat alle modeller",
|
||||||
|
"aiProviderModelsAddAllBlock": "Blokker alle modeller",
|
||||||
|
"aiProviderModelsAddAllDescription": "Bruker jokertegnet * slik at hver modellenøkkel matcher.",
|
||||||
|
"aiProviderModelsViewMore": "Vis mer ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Vis mindre",
|
||||||
|
"aiProviderModelsRemove": "Fjern modell",
|
||||||
|
"aiProviderModelsEditHint": "Klikk for å redigere modellinnstillinger",
|
||||||
|
"aiProviderModelsSourceCatalog": "Kjent katalogmodell",
|
||||||
|
"aiProviderModelsSourceCustom": "Tilpasset modellenøkkel",
|
||||||
|
"aiProviderModelsSourcePattern": "Jokertegnsmønster",
|
||||||
|
"aiProviderModelsSourceAll": "Matcher hver modellenøkkel",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Budsjett konfigurert",
|
||||||
|
"aiProviderModelsEditTitle": "Rediger Modell",
|
||||||
|
"aiProviderModelsEditDescription": "Oppdater modellenøkkelen eller konfigurer budsjettet.",
|
||||||
|
"aiProviderModelsBudgetTab": "Budsjett",
|
||||||
|
"aiProviderModelsKeyLabel": "Modell Nøkkel",
|
||||||
|
"aiProviderModelsKeyRequired": "Skriv inn en modellenøkkel",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Denne modellenøkkelen er allerede på en liste",
|
||||||
|
"aiProviderModelsOverlapError": "Disse mønstrene kan ikke være på begge listene: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modeller oppdatert",
|
||||||
|
"aiProviderModelsErrorUpdate": "Kunne ikke oppdatere modeller",
|
||||||
|
"aiResourceProviders": "Leverandører",
|
||||||
|
"aiResourceProvidersDescription": "Velg hvilke AI-leverandører denne AI-portalen kan bruke",
|
||||||
|
"aiResourceProvidersHelp": "Fest leverandører og velg arv (bruk hver leverandørlister) eller velg (velg en tillatliste for denne ressursen). Tillatmønstre som er i konflikt på tvers av vedlagte leverandører er ikke tillatt.",
|
||||||
|
"aiResourceProvidersSelect": "Velg leverandører",
|
||||||
|
"aiResourceProvidersEmpty": "Ingen AI-leverandører funnet",
|
||||||
|
"aiResourceProvidersNoneAttached": "Ingen leverandører knyttet ennå.",
|
||||||
|
"aiResourceProvidersAdd": "Legg til leverandør",
|
||||||
|
"aiResourceProvidersRemove": "Fjern leverandør",
|
||||||
|
"aiResourceProviderToggleEnabled": "Aktiver eller deaktiver denne leverandøren på ressursen",
|
||||||
|
"aiResourceProviderDisabled": "Deaktivert",
|
||||||
|
"aiResourceProvidersUpdated": "Leverandører oppdatert",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Kunne ikke oppdatere leverandører",
|
||||||
|
"aiResourceProviderEditDescription": "Velg hvordan denne leverandørens modeller er eksponert på denne ressursen.",
|
||||||
|
"viewProviderSettings": "Vis Leverandørinnstillinger",
|
||||||
|
"aiResourceProviderMode": "Tilgangsmodus",
|
||||||
|
"aiResourceProviderModeInherit": "Arve",
|
||||||
|
"aiResourceProviderModeSelect": "Velg",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Velg · {count} modeller",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Bruk denne leverandørens tillat- og blokk-lister som konfigurert på leverandøren.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Velg en delmengde av denne leverandørens tillat-listemodeller for denne ressursen.",
|
||||||
|
"aiResourceProviderAllowModels": "Tillat liste",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Velg modeller",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Søk modeller...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Ingen modeller funnet",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Bare modeller fra denne leverandørens tillat liste kan velges.",
|
||||||
|
"aiResourceAliasRequired": "Alias er påkrevd for AI-portaler",
|
||||||
|
"aiResourceDomainConfiguration": "Domene Konfigurasjon",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Velg domenet kunder vil bruke for å nå denne AI-portalen.",
|
||||||
|
"aiUsageAnalyticsTitle": "AI Bruksanalyse",
|
||||||
|
"aiUsageAnalyticsDescription": "Analyser AI-gatewaykostnad, tokenbruk og aktivitet på tvers av leverandører, ressurser, roller og brukere",
|
||||||
|
"aiUsageTabOverview": "Oversikt",
|
||||||
|
"aiUsageTabProviders": "Leverandører",
|
||||||
|
"aiUsageTabResources": "Ressurser",
|
||||||
|
"aiUsageFilterProvider": "Leverandør",
|
||||||
|
"aiUsageFilterModel": "Modell",
|
||||||
|
"aiUsageFilterResource": "Ressurs",
|
||||||
|
"aiUsageFilterRole": "Rolle",
|
||||||
|
"aiUsageFilterUser": "Bruker",
|
||||||
|
"aiUsageFilterAllProviders": "Alle Leverandører",
|
||||||
|
"aiUsageFilterAllModels": "Alle Modeller",
|
||||||
|
"aiUsageFilterAllResources": "Alle Ressurser",
|
||||||
|
"aiUsageFilterAllRoles": "Alle Roller",
|
||||||
|
"aiUsageFilterAllUsers": "Alle Brukere",
|
||||||
|
"aiUsageFilterSearch": "Søk...",
|
||||||
|
"aiUsageFilterNotFound": "Ingen alternativer funnet",
|
||||||
|
"aiUsageResetFilters": "Tilbakestill Filtre",
|
||||||
|
"aiUsageRefresh": "Oppdater",
|
||||||
|
"aiUsageTokenTypePrompt": "Stikkord",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Cache-lesing",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Cache-skriving",
|
||||||
|
"aiUsageTokenTypeCompletion": "Fullføring",
|
||||||
|
"aiUsageTokenTypeReasoning": "Resonnering",
|
||||||
|
"aiUsageRequests": "Forespørsler",
|
||||||
|
"aiUsageCost": "Kostnad",
|
||||||
|
"aiUsageTokens": "Token",
|
||||||
|
"aiUsageOther": "Annet",
|
||||||
|
"aiUsageTotalRequests": "Totale Forespørsler",
|
||||||
|
"aiUsageTotalTokens": "Totale Token",
|
||||||
|
"aiUsageTotalCost": "Totale Kostnad",
|
||||||
|
"aiUsageEstimated": "Anslått",
|
||||||
|
"aiUsageRequestVolume": "Forespørselsvolum",
|
||||||
|
"aiUsageTokenUsage": "Tokenforbruk",
|
||||||
|
"aiUsageModelCost": "Modellkostnad",
|
||||||
|
"aiUsageModelTokens": "Modelltoken",
|
||||||
|
"aiUsageTopModels": "Toppmodeller",
|
||||||
|
"aiUsageTopProviders": "Topp Leverandører",
|
||||||
|
"aiUsageProviderCost": "Leverandørkostnad",
|
||||||
|
"aiUsageProviderTokenUsage": "Leverandør Tokenforbruk",
|
||||||
|
"aiUsageTopResources": "Top Ressurser",
|
||||||
|
"aiUsageResourceCost": "Ressurskostnad",
|
||||||
|
"aiUsageResourceTokenUsage": "Ressurs Tokenforbruk",
|
||||||
|
"aiUsageResourceTypePublic": "Offentlig Ressurs",
|
||||||
|
"aiUsageResourceTypeSite": "Privat Ressurs",
|
||||||
|
"aiUsageNoResource": "Ingen ressurs",
|
||||||
|
"aiUsageRolesTab": "Roller",
|
||||||
|
"aiUsageUsersTab": "Brukere",
|
||||||
|
"aiUsageTopRoles": "Topproller",
|
||||||
|
"aiUsageRoleCost": "Rollekostnad",
|
||||||
|
"aiUsageRoleTokenUsage": "Rolle Tokenforbruk",
|
||||||
|
"aiUsageTopUsers": "Toppbrukere",
|
||||||
|
"aiUsageUserCost": "Brukerkostnad",
|
||||||
|
"aiUsageUserTokenUsage": "Bruker Tokenforbruk",
|
||||||
|
"aiUsageUnknownUser": "Ukjent bruker",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Virtuelle API-nøkler",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Virtuell API-nøkkel",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Alle Virtuelle API-nøkler",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Topp Virtuelle API-nøkler",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Virtuell API-nøkkelkostnad",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Virtuell API-nøkkel Tokenforbruk",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Ingen virtuell API-nøkkel",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Uten navn nøkkel",
|
||||||
|
"aiUsageLoading": "Laster inn...",
|
||||||
|
"aiUsageNoData": "Ingen data",
|
||||||
|
"resourceBudgetSettings": "Budsjett",
|
||||||
|
"resourceBudgetSettingsDescription": "Konfigurer hvordan denne AI-portalen begrenser bruk basert på utgifter eller token-grenser",
|
||||||
"sidebarApiKeys": "API-nøkler",
|
"sidebarApiKeys": "API-nøkler",
|
||||||
"sidebarProvisioning": "Levering",
|
"sidebarProvisioning": "Levering",
|
||||||
"sidebarSettings": "Innstillinger",
|
"sidebarSettings": "Innstillinger",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Subnett",
|
"subnetPlaceholder": "Subnett",
|
||||||
"addressDescription": "Den interne adressen til klienten. Må falle innenfor organisasjonens undernett.",
|
"addressDescription": "Den interne adressen til klienten. Må falle innenfor organisasjonens undernett.",
|
||||||
"selectSites": "Velg områder",
|
"selectSites": "Velg områder",
|
||||||
|
"selectResources": "Velg ressurser",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# ressurs} other {# ressurser}}",
|
||||||
"selectLabels": "Velg etiketter",
|
"selectLabels": "Velg etiketter",
|
||||||
"sitesDescription": "Klienten vil ha tilkobling til de valgte områdene",
|
"sitesDescription": "Klienten vil ha tilkobling til de valgte områdene",
|
||||||
"clientInstallOlm": "Installer Olm",
|
"clientInstallOlm": "Installer Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Tid er i sekunder",
|
"timeIsInSeconds": "Tid er i sekunder",
|
||||||
"requireDeviceApproval": "Krev enhetsgodkjenning",
|
"requireDeviceApproval": "Krev enhetsgodkjenning",
|
||||||
"requireDeviceApprovalDescription": "Brukere med denne rollen trenger nye enheter godkjent av en admin før de kan koble seg og få tilgang til ressurser.",
|
"requireDeviceApprovalDescription": "Brukere med denne rollen trenger nye enheter godkjent av en admin før de kan koble seg og få tilgang til ressurser.",
|
||||||
"sshSettings": "SSH Innstillinger",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI Portal",
|
||||||
"sshAccess": "SSH-tilgang",
|
"sshAccess": "SSH-tilgang",
|
||||||
"rdpSettings": "RDP Innstillinger",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "VNC Innstillinger",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH-server",
|
"sshServer": "SSH-server",
|
||||||
"rdpServer": "RDP-server",
|
"rdpServer": "RDP-server",
|
||||||
"vncServer": "VNC-server",
|
"vncServer": "VNC-server",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI Portal",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Skjema",
|
"editInternalResourceDialogScheme": "Skjema",
|
||||||
"editInternalResourceDialogEnableSsl": "Aktiver TLS",
|
"editInternalResourceDialogEnableSsl": "Aktiver TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Tillat ICMP Ping",
|
"privateResourceAllowIcmpPing": "Tillat ICMP Ping",
|
||||||
"privateResourceNetworkAccess": "Nettverkstilgang",
|
"privateResourceNetworkAccess": "Nettverkstilgang",
|
||||||
"privateResourceNetworkAccessDescription": "Kontroller TCP/UDP porttilgang og om ICMP ping tillates for denne ressursen.",
|
"privateResourceNetworkAccessDescription": "Kontroller TCP/UDP porttilgang og om ICMP ping tillates for denne ressursen.",
|
||||||
"hostSettings": "Vertinnstillinger",
|
"hostSettings": "Vert",
|
||||||
"cidrSettings": "CIDR-innstillinger",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Ressursegenskaper",
|
"createInternalResourceDialogResourceProperties": "Ressursegenskaper",
|
||||||
"createInternalResourceDialogName": "Navn",
|
"createInternalResourceDialogName": "Navn",
|
||||||
"createInternalResourceDialogSite": "Område",
|
"createInternalResourceDialogSite": "Område",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI Portal",
|
||||||
"scheme": "Skjema",
|
"scheme": "Skjema",
|
||||||
"createInternalResourceDialogScheme": "Skjema",
|
"createInternalResourceDialogScheme": "Skjema",
|
||||||
"createInternalResourceDialogEnableSsl": "Aktiver TLS",
|
"createInternalResourceDialogEnableSsl": "Aktiver TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Gyldig passord",
|
"validPassword": "Gyldig passord",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Gyldig Virtuell API-nøkkel",
|
||||||
"view": "Vis",
|
"view": "Vis",
|
||||||
"configManaged": "Konfigurasjon administrert",
|
"configManaged": "Konfigurasjon administrert",
|
||||||
"connectedClient": "Tilkoblet klient",
|
"connectedClient": "Tilkoblet klient",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Logg tilbaketrekning",
|
"logRetention": "Logg tilbaketrekning",
|
||||||
"logRetentionDescription": "Håndter hvor lenge ulike typer logger beholdes for denne organisasjonen, eller deaktiver dem",
|
"logRetentionDescription": "Håndter hvor lenge ulike typer logger beholdes for denne organisasjonen, eller deaktiver dem",
|
||||||
"requestLogsDescription": "Se detaljerte forespørselslogger for ressurser i denne organisasjonen",
|
"requestLogsDescription": "Se detaljerte forespørselslogger for ressurser i denne organisasjonen",
|
||||||
|
"aiSessionLogs": "AI Portal Sesjonslogger",
|
||||||
|
"aiSessionLogsDescription": "Vis stikkord- og responsutskrifter for AI-portal forespørsler i denne organisasjonen",
|
||||||
|
"sidebarLogsAi": "Sesjonslogger",
|
||||||
|
"commandLogsAi": "Sesjonslogger",
|
||||||
|
"sidebarLogsAiUsage": "Bruksanalyse",
|
||||||
|
"commandLogsAiUsage": "Bruksanalyse",
|
||||||
|
"provider": "Leverandør",
|
||||||
|
"capability": "Kapasitet",
|
||||||
|
"model": "Modell",
|
||||||
|
"virtualApiKey": "Virtuell API-nøkkel",
|
||||||
|
"noVirtualApiKey": "Ingen virtuell API-nøkkel",
|
||||||
|
"stream": "Strøm",
|
||||||
|
"streaming": "Strømming",
|
||||||
|
"nonStreaming": "Ikke-strømming",
|
||||||
|
"statusCode": "Statuskode",
|
||||||
|
"aiSessionId": "Sesjons ID",
|
||||||
|
"aiSessionRequest": "Forespørsel",
|
||||||
|
"aiSessionResponse": "Respons",
|
||||||
|
"aiSessionNoData": "Ingen data registrert",
|
||||||
|
"aiSessionCouldNotParse": "(rå, kunne ikke analysere utskrift)",
|
||||||
|
"aiSessionLogTruncated": "Denne sesjonen ble avkortet før lagring og kan være ufullstendig.",
|
||||||
|
"aiSessionViewRaw": "Vis Rå JSON",
|
||||||
|
"aiSessionViewChat": "Vis Chat",
|
||||||
|
"cost": "Kostnad",
|
||||||
|
"estimated": "Anslått",
|
||||||
|
"tokenUsage": "Tokenforbruk",
|
||||||
|
"promptTokens": "Stikkord Token",
|
||||||
|
"cacheReadTokens": "Cache-lesing Token",
|
||||||
|
"cacheWriteTokens": "Cache-skriving Token",
|
||||||
|
"completionTokens": "Fullføring Token",
|
||||||
|
"reasoningTokens": "Resonnering Token",
|
||||||
|
"totalTokens": "Totale Token",
|
||||||
"requestAnalyticsDescription": "Se detaljert rekvisisjonsanalyse for ressurser i denne organisasjonen",
|
"requestAnalyticsDescription": "Se detaljert rekvisisjonsanalyse for ressurser i denne organisasjonen",
|
||||||
"logRetentionRequestLabel": "Be om loggbevaring",
|
"logRetentionRequestLabel": "Be om loggbevaring",
|
||||||
"logRetentionRequestDescription": "Hvor lenge du vil beholde forespørselslogger",
|
"logRetentionRequestDescription": "Hvor lenge du vil beholde forespørselslogger",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Hvor lenge handlingen skal lagres",
|
"logRetentionActionDescription": "Hvor lenge handlingen skal lagres",
|
||||||
"logRetentionConnectionLabel": "Logg nyhet",
|
"logRetentionConnectionLabel": "Logg nyhet",
|
||||||
"logRetentionConnectionDescription": "Hvor lenge du vil beholde tilkoblingslogger",
|
"logRetentionConnectionDescription": "Hvor lenge du vil beholde tilkoblingslogger",
|
||||||
|
"logRetentionAISessionsLabel": "Bevaringsinnstillinger AI-portal sesjonslogger",
|
||||||
|
"logRetentionAISessionsDescription": "Hvor lenge man skal bevare stikkord/response sesjonslogger for AI-portaler",
|
||||||
"logRetentionDisabled": "Deaktivert",
|
"logRetentionDisabled": "Deaktivert",
|
||||||
"logRetention3Days": "3 dager",
|
"logRetention3Days": "3 dager",
|
||||||
"logRetention7Days": "7 dager",
|
"logRetention7Days": "7 dager",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Kilde adresse",
|
"sourceAddress": "Kilde adresse",
|
||||||
"destinationAddress": "Måladresse (Automatic Translation)",
|
"destinationAddress": "Måladresse (Automatic Translation)",
|
||||||
"duration": "Varighet",
|
"duration": "Varighet",
|
||||||
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er nødvendig for å bruke denne funksjonen. <bookADemoLink>Bestill en gratis demo eller POC prøve for å lære mer.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition lisens</enterpriseEditionLink> er påkrevd. <bookADemoLink>Bestill en demo eller prøveversjon</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Bestill en gratis demo eller POC prøve for å lære mer.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition lisens</enterpriseEditionLink> er påkrevd. <bookADemoLink>Bestill en demo eller prøveversjon</bookADemoLink>",
|
||||||
"certResolver": "Sertifikat løser",
|
"certResolver": "Sertifikat løser",
|
||||||
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
|
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
|
||||||
"selectCertResolver": "Velg sertifikatløser",
|
"selectCertResolver": "Velg sertifikatløser",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Valg av flere nettsteder muliggjør motstandskraftig ruting og failover for høy tilgjengelighet.",
|
"internalResourceFormMultiSiteRoutingHelp": "Valg av flere nettsteder muliggjør motstandskraftig ruting og failover for høy tilgjengelighet.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Lær mer",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Lær mer",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Begrens tilgang til spesifikke TCP/UDP-porter eller tillate/blokkere alle porter.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Begrens tilgang til spesifikke TCP/UDP-porter eller tillate/blokkere alle porter.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "HTTP-konfigurasjon",
|
"createInternalResourceDialogHttpConfiguration": "Domene Konfigurasjon",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Velg domenet klienter vil bruke for å nå denne ressursen via HTTP eller HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Velg domenet klienter vil bruke for å nå denne ressursen via HTTP eller HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "HTTP-konfigurasjon",
|
"editInternalResourceDialogHttpConfiguration": "Domene Konfigurasjon",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Velg domenet klienter vil bruke for å nå denne ressursen via HTTP eller HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Velg domenet klienter vil bruke for å nå denne ressursen via HTTP eller HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Umerket",
|
"resourceLauncherUnlabeled": "Umerket",
|
||||||
|
"resourceLauncherAiGateway": "AI Portal",
|
||||||
"resourceLauncherNoSite": "Ingen område",
|
"resourceLauncherNoSite": "Ingen område",
|
||||||
|
"resourceLauncherAvailableModels": "Tilgjengelige Modeller",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modeller du kan bruke med denne AI-portalen.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Ingen modeller er tilgjengelige for denne ressursen.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Kunne ikke laste inn tilgjengelige modeller.",
|
||||||
|
"resourceLauncherApiKeys": "API-nøkler",
|
||||||
|
"resourceLauncherApiKeysDescription": "Bruk din identitetsnøkkel eller en tillagt nøkkel for å autentisere med denne ressursen.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Identitetsnøkkel",
|
||||||
|
"resourceLauncherApiKeysManual": "Tildelte nøkler",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Ingen API-nøkler er tilgjengelige for denne ressursen.",
|
||||||
|
"resourceLauncherApiKeysError": "Kunne ikke laste inn API-nøkler.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Ingen ressurser i denne gruppen",
|
"resourceLauncherNoResourcesInGroup": "Ingen ressurser i denne gruppen",
|
||||||
"resourceLauncherEmptyStateTitle": "Ingen tilgjengelige ressurser",
|
"resourceLauncherEmptyStateTitle": "Ingen tilgjengelige ressurser",
|
||||||
"resourceLauncherEmptyStateDescription": "Du har ennå ikke tilgang til noen ressurser. Kontakt administratoren din for å be om tilgang.",
|
"resourceLauncherEmptyStateDescription": "Du har ennå ikke tilgang til noen ressurser. Kontakt administratoren din for å be om tilgang.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Kunne ikke slette lanseringsvisningen. Vennligst prøv igjen.",
|
"resourceLauncherViewDeleteFailedDescription": "Kunne ikke slette lanseringsvisningen. Vennligst prøv igjen.",
|
||||||
"memberPortalPrevious": "Forrige",
|
"memberPortalPrevious": "Forrige",
|
||||||
"memberPortalNext": "Neste",
|
"memberPortalNext": "Neste",
|
||||||
"httpSettings": "HTTP Innstillinger",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "TCP Innstillinger",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "UDP Innstillinger",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Opprette en sikker tilkobling…",
|
"sshConnectingDescription": "Opprette en sikker tilkobling…",
|
||||||
"sshConnecting": "Kobler til…",
|
"sshConnecting": "Kobler til…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Controleer of je bent aangemeld als de juiste gebruiker.",
|
"inviteLoginUser": "Controleer of je bent aangemeld als de juiste gebruiker.",
|
||||||
"inviteErrorNoUser": "Het spijt ons, maar de uitnodiging die u probeert te gebruiken is niet voor een bestaande gebruiker.",
|
"inviteErrorNoUser": "Het spijt ons, maar de uitnodiging die u probeert te gebruiken is niet voor een bestaande gebruiker.",
|
||||||
"inviteCreateUser": "U moet eerst een account aanmaken.",
|
"inviteCreateUser": "U moet eerst een account aanmaken.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Uitnodigingen kunnen alleen door interne accounts worden geaccepteerd. Log uit en log in met uw wachtwoord voor dit e-mailadres.",
|
||||||
|
"inviteLoginInternalOnly": "Uitnodigingen vereisen een intern account met een wachtwoord. Maak een account aan of meld u aan met uw wachtwoord.",
|
||||||
"goHome": "Ga naar huis",
|
"goHome": "Ga naar huis",
|
||||||
"inviteLogInOtherUser": "Log in als een andere gebruiker",
|
"inviteLogInOtherUser": "Log in als een andere gebruiker",
|
||||||
"createAnAccount": "Account aanmaken",
|
"createAnAccount": "Account aanmaken",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Geen land gevonden.",
|
"noCountryFound": "Geen land gevonden.",
|
||||||
"siteSelectionDescription": "Deze site zal connectiviteit met het doelwit bieden.",
|
"siteSelectionDescription": "Deze site zal connectiviteit met het doelwit bieden.",
|
||||||
"resourceType": "Type bron",
|
"resourceType": "Type bron",
|
||||||
"resourceTypeDescription": "Dit bepaalt het bronprotocol en hoe het in de browser wordt weergegeven. Dit kan later niet gewijzigd worden.",
|
"resourceTypeDescription": "Dit regelt het bronprotocol en kan later niet meer worden gewijzigd.",
|
||||||
|
"resourceTypeSearch": "Zoek bronentypes...",
|
||||||
|
"resourceTypeNotFound": "Geen bronentype gevonden",
|
||||||
|
"resourceTypeHttpDescription": "Proxy webverkeer via HTTPS met een domeinnaam",
|
||||||
|
"resourceTypeInferenceDescription": "Routeer AI-verzoeken via gekoppelde providers",
|
||||||
|
"resourceTypeSshDescription": "Benader een SSH-server vanuit de browser",
|
||||||
|
"resourceTypeRdpDescription": "Benader een externe desktop vanuit de browser",
|
||||||
|
"resourceTypeVncDescription": "Benader een VNC-desktop vanuit de browser",
|
||||||
|
"resourceTypeTcpDescription": "Proxy rauw TCP-verkeer via een poortnummer",
|
||||||
|
"resourceTypeUdpDescription": "Proxy rauw UDP-verkeer via een poortnummer",
|
||||||
|
"privateResourceTypeDescription": "Dit regelt hoe clients de bron bereiken. Dit kan later niet meer worden gewijzigd.",
|
||||||
|
"privateResourceTypeHostDescription": "Stel een enkele host op het sitenetwerk bloot aan verbonden cliënten",
|
||||||
|
"privateResourceTypeCidrDescription": "Stel een CIDR-bereik op het sitenetwerk bloot aan verbonden cliënten",
|
||||||
|
"privateResourceTypeHttpDescription": "Benader een HTTP- of HTTPS-dienst via een domein",
|
||||||
|
"privateResourceTypeSshDescription": "Benader een SSH-server vanuit verbonden cliënten",
|
||||||
"resourceDomainDescription": "De bron zal worden bediend onder deze volledig gekwalificeerde domeinnaam.",
|
"resourceDomainDescription": "De bron zal worden bediend onder deze volledig gekwalificeerde domeinnaam.",
|
||||||
"resourceHTTPSSettings": "HTTPS instellingen",
|
"resourceHTTPSSettings": "HTTPS instellingen",
|
||||||
"resourceHTTPSSettingsDescription": "Stel in hoe de bron wordt benaderd via HTTPS",
|
"resourceHTTPSSettingsDescription": "Stel in hoe de bron wordt benaderd via HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Instellingen opslaan",
|
"authMethodsSave": "Instellingen opslaan",
|
||||||
"policyAuthStackTitle": "Authenticatie",
|
"policyAuthStackTitle": "Authenticatie",
|
||||||
"policyAuthStackDescription": "Bepaal welke authenticatiemethoden vereist zijn om toegang tot deze bron te krijgen",
|
"policyAuthStackDescription": "Bepaal welke authenticatiemethoden vereist zijn om toegang tot deze bron te krijgen",
|
||||||
|
"policyAuthInferenceStackDescription": "Kies welke gebruikers en rollen kunnen authenticeren bij deze AI-gateway",
|
||||||
"policyAuthOrLogicTitle": "Meerdere authenticatiemethoden actief",
|
"policyAuthOrLogicTitle": "Meerdere authenticatiemethoden actief",
|
||||||
"policyAuthOrLogicBanner": "Bezoekers kunnen zich aanmelden met een van de hieronder actieve methoden. Ze hoeven ze niet allemaal te voltooien.",
|
"policyAuthOrLogicBanner": "Bezoekers kunnen zich aanmelden met een van de hieronder actieve methoden. Ze hoeven ze niet allemaal te voltooien.",
|
||||||
"policyAuthMethodActive": "Actief",
|
"policyAuthMethodActive": "Actief",
|
||||||
"policyAuthMethodOff": "Uit",
|
"policyAuthMethodOff": "Uit",
|
||||||
"policyAuthSsoTitle": "Platform SSO",
|
"policyAuthSsoTitle": "Platform SSO",
|
||||||
"policyAuthSsoDescription": "Vereis inloggen via de identiteit provider van uw organisatie",
|
"policyAuthSsoDescription": "Vereis inloggen via de identiteit provider van uw organisatie",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "Aanmeldings-URL",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Elke gebruiker heeft al een identity-API-sleutel, dus u hoeft alleen virtuele API-sleutels te maken voor niet-gebruikersclients of gedeelde toegang. Gebruikers kunnen hun sleutel ophalen door in te loggen met hun aanbieder van identiteit bij de URL van deze bron, waar deze na het inloggen wordt getoond.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Elke gebruiker heeft al een identity-API-sleutel, dus u hoeft alleen virtuele API-sleutels te maken voor niet-gebruikersclients of gedeelde toegang. Gebruikers halen hun sleutel hier op na inloggen met hun identiteit-aanbieder.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} gebruikers, {roles} rollen",
|
"policyAuthSsoSummary": "{idp} · {users} gebruikers, {roles} rollen",
|
||||||
"policyAuthSsoDefaultIdp": "Standaard provider",
|
"policyAuthSsoDefaultIdp": "Standaard provider",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Standaard Identiteit Provider Toevoegen",
|
"policyAuthAddDefaultIdentityProvider": "Standaard Identiteit Provider Toevoegen",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Nieuwe rol is vereist",
|
"accessRoleErrorNewRequired": "Nieuwe rol is vereist",
|
||||||
"accessRoleErrorRemove": "Rol verwijderen mislukt",
|
"accessRoleErrorRemove": "Rol verwijderen mislukt",
|
||||||
"accessRoleErrorRemoveDescription": "Er is een fout opgetreden tijdens het verwijderen van de rol.",
|
"accessRoleErrorRemoveDescription": "Er is een fout opgetreden tijdens het verwijderen van de rol.",
|
||||||
|
"accessRoleInferenceBudget": "AI-budget",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Configureer hoe leden van deze rol AI-gebruik beperken op basis van uitgaven of tokenlimieten",
|
||||||
"accessRoleName": "Rol naam",
|
"accessRoleName": "Rol naam",
|
||||||
"accessRoleQuestionRemove": "Je staat op het punt de `{name}` rol te verwijderen. Je kunt deze actie niet ongedaan maken.",
|
"accessRoleQuestionRemove": "Je staat op het punt de `{name}` rol te verwijderen. Je kunt deze actie niet ongedaan maken.",
|
||||||
"accessRoleRemove": "Rol verwijderen",
|
"accessRoleRemove": "Rol verwijderen",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Site verwijderen",
|
"actionDeleteSite": "Site verwijderen",
|
||||||
"actionGetSite": "Site ophalen",
|
"actionGetSite": "Site ophalen",
|
||||||
"actionListSites": "Sites weergeven",
|
"actionListSites": "Sites weergeven",
|
||||||
|
"actionCreateAiProvider": "AI-provider aanmaken",
|
||||||
|
"actionDeleteAiProvider": "Verwijder AI-provider",
|
||||||
|
"actionGetAiProvider": "AI-provider ophalen",
|
||||||
|
"actionListAiProviders": "AI-providers weergeven",
|
||||||
|
"actionUpdateAiProvider": "AI-provider bijwerken",
|
||||||
|
"actionCreateAiModel": "AI-model aanmaken",
|
||||||
|
"actionDeleteAiModel": "AI-model verwijderen",
|
||||||
|
"actionGetAiModel": "AI-model ophalen",
|
||||||
|
"actionListAiModels": "AI-modellen weergeven",
|
||||||
|
"actionUpdateAiModel": "AI-model bijwerken",
|
||||||
|
"actionCreateAiBudget": "AI-budget aanmaken",
|
||||||
|
"actionDeleteAiBudget": "AI-budget verwijderen",
|
||||||
|
"actionGetAiBudget": "AI-budget ophalen",
|
||||||
|
"actionListAiBudgets": "AI-budgetten weergeven",
|
||||||
|
"actionUpdateAiBudget": "AI-budget bijwerken",
|
||||||
|
"actionListResourceAiModels": "Bron AI-modellen weergeven",
|
||||||
|
"actionSetResourceAiModels": "Bron AI-modellen instellen",
|
||||||
|
"actionCreateVirtualApiKey": "Virtuele API-sleutel aanmaken",
|
||||||
|
"actionDeleteVirtualApiKey": "Virtuele API-sleutel verwijderen",
|
||||||
|
"actionGetVirtualApiKey": "Virtuele API-sleutel ophalen",
|
||||||
|
"actionListVirtualApiKeys": "Virtuele API-sleutels weergeven",
|
||||||
|
"actionUpdateVirtualApiKey": "Virtuele API-sleutel bijwerken",
|
||||||
"actionApplyBlueprint": "Blauwdruk toepassen",
|
"actionApplyBlueprint": "Blauwdruk toepassen",
|
||||||
"actionListBlueprints": "Lijst blauwdrukken",
|
"actionListBlueprints": "Lijst blauwdrukken",
|
||||||
"actionGetBlueprint": "Krijg Blauwdruk",
|
"actionGetBlueprint": "Krijg Blauwdruk",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Stel Resource Pincode in",
|
"actionSetResourcePincode": "Stel Resource Pincode in",
|
||||||
"actionSetResourceEmailWhitelist": "Stel Resource e-mail whitelist in",
|
"actionSetResourceEmailWhitelist": "Stel Resource e-mail whitelist in",
|
||||||
"actionGetResourceEmailWhitelist": "Verkrijg Resource E-mail Whitelist",
|
"actionGetResourceEmailWhitelist": "Verkrijg Resource E-mail Whitelist",
|
||||||
|
"actionListResourcePolicies": "Lijst bronbeleid",
|
||||||
|
"actionCreateResourcePolicy": "Bronbeleid aanmaken",
|
||||||
"actionGetResourcePolicy": "Haal Bronbeleid op",
|
"actionGetResourcePolicy": "Haal Bronbeleid op",
|
||||||
"actionUpdateResourcePolicy": "Werk Bronbeleid bij",
|
"actionUpdateResourcePolicy": "Werk Bronbeleid bij",
|
||||||
|
"actionDeleteResourcePolicy": "Verwijder bronbeleid",
|
||||||
"actionSetResourcePolicyUsers": "Stel gebruikers van bronbeleid in",
|
"actionSetResourcePolicyUsers": "Stel gebruikers van bronbeleid in",
|
||||||
"actionSetResourcePolicyRoles": "Stel rollen van bronbeleid in",
|
"actionSetResourcePolicyRoles": "Stel rollen van bronbeleid in",
|
||||||
"actionSetResourcePolicyPassword": "Stel wachtwoord van bronbeleid in",
|
"actionSetResourcePolicyPassword": "Stel wachtwoord van bronbeleid in",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Uitnodigingen",
|
"sidebarInvitations": "Uitnodigingen",
|
||||||
"sidebarRoles": "Rollen",
|
"sidebarRoles": "Rollen",
|
||||||
"sidebarShareableLinks": "Deelbare Links",
|
"sidebarShareableLinks": "Deelbare Links",
|
||||||
|
"sidebarAiGateway": "AI-gateway",
|
||||||
|
"sidebarAiProviders": "Aanbieders",
|
||||||
|
"commandAiProviders": "AI-aanbieders",
|
||||||
|
"sidebarVirtualApiKeys": "Virtuele API-sleutels",
|
||||||
|
"sidebarMyApiKeys": "Uw API-sleutels",
|
||||||
|
"sidebarAccount": "Startpagina",
|
||||||
|
"commandVirtualApiKeys": "Virtuele API-sleutels",
|
||||||
|
"virtualApiKeysTitle": "Virtuele API-sleutels beheren",
|
||||||
|
"virtualApiKeysDescription": "Maak en beheer handmatige API-sleutels voor toegang tot AI-gateway van openbare AI-gateways",
|
||||||
|
"virtualApiKeysTabIdentity": "Identity-sleutels",
|
||||||
|
"virtualApiKeysTabVirtual": "Virtuele sleutels",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Identity-sleutels voor elke gebruiker",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Elke gebruiker heeft al een Pangolin identity-sleutel voor deze organisatie. Het is uniek voor hun account en identificeert hen bij AI-gateways waartoe ze toegang hebben.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Voorbeeld",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Hoe gebruikers hun sleutel krijgen",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Bezoek een openbare AI-gateway-URL in de browser en log in met hun account.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Of ga naar <url></url> terwijl u bent ingelogd.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "U kunt hier extra virtuele API-sleutels maken op het tabblad Virtuele sleutels. Die sleutels kunnen worden toegespitst op specifieke openbare AI-gateways en optioneel worden geassocieerd met een gebruiker. Het aanmaken van een sleutel geeft meteen toegang tot de geselecteerde openbare AI-gateways.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Maak handmatig een sleutel aan",
|
||||||
|
"virtualApiKeysEmailIdentity": "Email identity-sleutels",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Verstuur elke geselecteerde gebruiker of rol hun Pangolin identity-sleutel.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Verstuur naar alle gebruikers",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Email elk lid van de organisatie die een account-email heeft.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Gebruikers",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Rollen",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Verstuur emails",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Identity-sleutels gemaild",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Er zijn {sent} e-mails verzonden.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} gebruikers werden overgeslagen omdat ze geen e-mailadres hebben.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Selecteer ten minste één gebruiker of rol, of verzend naar alle gebruikers.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Fout bij verzenden van identity-sleutels",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Emailen van identity-sleutels is mislukt",
|
||||||
|
"virtualApiKeysBannerTitle": "Identity-sleutels voor elke gebruiker",
|
||||||
|
"virtualApiKeysBannerDescription": "Elke gebruiker heeft al een identity-sleutel beschikbaar op {keysUrl}. U kunt hier ook handmatig sleutels genereren die directe toegang verlenen tot openbare AI-gateways.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Bekijk identity-sleutels",
|
||||||
|
"virtualApiKeys": "Virtuele API-sleutels",
|
||||||
|
"virtualApiKeysSearch": "Zoek sleutels...",
|
||||||
|
"virtualApiKeysCreate": "Virtuele API-sleutel aanmaken",
|
||||||
|
"virtualApiKeysCreateDescription": "Maak een handmatige sleutel aan die openbare AI-gateways in deze organisatie kan bellen",
|
||||||
|
"virtualApiKeysCreateButton": "Sleutel aanmaken",
|
||||||
|
"virtualApiKeysEmpty": "Nog geen virtuele API-sleutels beschikbaar",
|
||||||
|
"virtualApiKeysName": "Naam",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Omschrijving (optioneel)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Gebruiker toewijzen (optioneel)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Wijs deze sleutel toe aan een gebruiker om het gebruik bij te houden. Zodra deze is aangemaakt, geeft deze sleutel direct toegang tot de geselecteerde openbare AI-gateways. U hoeft geen gebruiker handmatig aan de resource toe te wijzen. De sleutel is ook zichtbaar in het gebruikersprofiel.",
|
||||||
|
"virtualApiKeysAllResources": "Alle openbare AI-gateways",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Laat deze sleutel toegang krijgen tot elke openbare AI-gateway in de organisatie",
|
||||||
|
"virtualApiKeysSelectResources": "Openbare AI-gateways",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Selecteer bronnen",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Kies tot welke openbare AI-gateways deze sleutel toegang kan krijgen",
|
||||||
|
"virtualApiKeysNoResources": "Geen bronnen",
|
||||||
|
"virtualApiKeysSecret": "Sleutel",
|
||||||
|
"virtualApiKeysCopyKey": "Kopieer deze sleutel. Je kunt het later opnieuw bekijken vanuit de tabel of bij het bewerken.",
|
||||||
|
"virtualApiKeysViewSecret": "Bekijk geheim",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Secret van virtuele API-sleutel",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Dit geheim geeft toegang tot de openbare AI-gateways die aan deze sleutel zijn toegewezen",
|
||||||
|
"virtualApiKeysEdit": "Virtuele API-sleutel bewerken",
|
||||||
|
"virtualApiKeysEditDescription": "Werk de toegewezen gebruiker en toegang tot openbare AI-gateways bij voor deze sleutel",
|
||||||
|
"virtualApiKeysSaveButton": "Wijzigingen opslaan",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Selecteer ten minste één openbare AI-gateway, of schakel alle openbare AI-gateways in",
|
||||||
|
"virtualApiKeysUpdated": "Virtuele API-sleutel bijgewerkt",
|
||||||
|
"virtualApiKeysUpdatedDescription": "De virtuele API-sleutel is bijgewerkt",
|
||||||
|
"virtualApiKeysErrorUpdate": "Fout bij het bijwerken van virtuele API-sleutel",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Mislukt om virtuele API-sleutel bij te werken",
|
||||||
|
"virtualApiKeysErrorCreate": "Fout bij maken van virtuele API-sleutel",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Mislukt om virtuele API-sleutel te maken",
|
||||||
|
"virtualApiKeysErrorDelete": "Fout bij verwijderen van virtuele API-sleutel",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Mislukt om virtuele API-sleutel te verwijderen",
|
||||||
|
"virtualApiKeysDeleted": "Virtuele API-sleutel verwijderd",
|
||||||
|
"virtualApiKeysDeletedDescription": "De virtuele API-sleutel is verwijderd",
|
||||||
|
"virtualApiKeysDelete": "Virtuele API-sleutel verwijderen",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Sleutel verwijderen",
|
||||||
|
"virtualApiKeysQuestionRemove": "Weet je zeker dat je deze virtuele API-sleutel wilt verwijderen?",
|
||||||
|
"virtualApiKeysMessageRemove": "Clients die deze sleutel gebruiken verliezen onmiddellijk de toegang.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Fout bij laden van geheim",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Mislukt om het geheim van virtuele API-sleutel te laden",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Niet toegewezen",
|
||||||
|
"virtualApiKeysInferenceBudget": "Budget",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Configureer hoe deze sleutel AI-gebruik beperkt op basis van uitgaven of tokenlimieten",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Email sleutel bij generatie",
|
||||||
|
"virtualApiKeysEmailThisKey": "Email deze sleutel",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Stuur de sleutel naar de toegewezen gebruiker en extra adressen nadat deze is aangemaakt",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Stuur de huidige sleutel naar de toegewezen gebruiker en extra adressen",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "Email is niet geconfigureerd op deze server",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Configureer SMTP om virtuele API-sleutels te emailen.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Stuur naar toegewezen gebruiker",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Email de sleutel naar het accountemail van de toegewezen gebruiker",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Wijs een gebruiker toe om de sleutel naar die gebruiker te sturen",
|
||||||
|
"virtualApiKeysEmailAdditional": "Aanvullende emails",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Voeg email toe en druk op Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Selecteer de toegewezen gebruiker of voeg ten minste één e-mailadres toe",
|
||||||
|
"myVirtualApiKeysTitle": "Uw API-sleutels",
|
||||||
|
"myVirtualApiKeysDescription": "Bekijk uw identiteitssleutel en alle virtuele API-sleutels die aan u zijn toegewezen in deze organisatie",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Uw API-sleutels voor {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Bekijk uw identiteitssleutel en virtuele API-sleutels die aan u zijn toegewezen die toegang kunnen krijgen tot {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Identity-sleutel",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Uw persoonlijke API-sleutel",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Uw persoonlijke sleutel voor deze organisatie. Het is uniek voor uw account en wordt gebruikt om u te identificeren bij het aanroepen van AI Gateway-resources.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Uw persoonlijke API-sleutel voor {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Uw persoonlijke sleutel voor deze organisatie. Gebruik het om {resourceName} aan te roepen.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Toegewezen sleutels",
|
||||||
|
"myVirtualApiKeysManualDescription": "Handmatige virtuele API-sleutels die een beheerder aan uw account heeft toegewezen",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Handmatige virtuele API-sleutels die aan uw account zijn gekoppeld en toegang kunnen krijgen tot {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Nog geen toegewezen sleutels",
|
||||||
|
"myVirtualApiKeysKindUser": "Identiteit",
|
||||||
|
"myVirtualApiKeysKindManual": "Handmatig",
|
||||||
|
"myVirtualApiKeysUnnamed": "Naamloos sleutel",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Geheim onthullen",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Dit geheim authenticeert u bij AI Gateway-resources",
|
||||||
|
"aiProvidersTitle": "AI-aanbieders",
|
||||||
|
"aiProvidersDescription": "Verbind modellenproviders voor AI-werklasten in deze organisatie",
|
||||||
|
"aiProvidersBannerTitle": "Modellenproviders verbinden",
|
||||||
|
"aiProvidersBannerDescription": "Providers zijn de modelbackends die Pangolin gebruikt voor AI-werklasten. Verbind OpenAI, Anthropic en andere providers hier, en verbind ze vervolgens aan openbare AI-gateways zodat gebruikers modellen kunnen oproepen met identiteit-bewuste toegang, budgetten en loggen.",
|
||||||
|
"aiProvidersAdd": "Voeg provider toe",
|
||||||
|
"aiProvidersSearch": "Zoek providers...",
|
||||||
|
"aiProvidersEmpty": "Nog geen AI-providers",
|
||||||
|
"aiProviderCreate": "AI-provider aanmaken",
|
||||||
|
"aiProviderCreateDescription": "Voeg een modellenprovider toe voor deze organisatie",
|
||||||
|
"aiProviderSeeAll": "Zie alle providers",
|
||||||
|
"aiProviderSetting": "Providerinstellingen voor {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Configureer deze AI-provider",
|
||||||
|
"aiProviderGeneral": "Algemeen",
|
||||||
|
"aiProviderGeneralDescription": "Basisinstellingen voor deze provider",
|
||||||
|
"aiProviderConfiguration": "Configuratie",
|
||||||
|
"aiProviderConfigurationDescription": "Netwerk routering en authenticatie voor deze provider",
|
||||||
|
"aiProviderNetworkSettings": "Netwerkinstellingen",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Kies hoe verkeer deze provider bereikt",
|
||||||
|
"aiProviderAuthSettings": "Authenticatie",
|
||||||
|
"aiProviderAuthSettingsDescription": "Configureer hoe deze provider verzoeken naar zijn upstream-URL authenticatie",
|
||||||
|
"aiProviderBudgetSettings": "Budget",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Configureer hoe deze provider gebruik beperkt op basis van uitgaven of tokenlimieten",
|
||||||
|
"aiBudgetAdd": "Budget toevoegen",
|
||||||
|
"aiBudgetEmpty": "Nog geen budgetten geconfigureerd. Klik op Budget toevoegen om een uitgave- of tokenlimiet in te stellen.",
|
||||||
|
"aiBudgetUnit": "Uitgave type",
|
||||||
|
"aiBudgetPeriod": "Reset periode",
|
||||||
|
"aiBudgetAmount": "Maximale uitgave",
|
||||||
|
"aiBudgetAmountPlaceholder": "Maximale uitgave",
|
||||||
|
"aiBudgetPeriodHourly": "Elk uur",
|
||||||
|
"aiBudgetPeriodDaily": "Dagelijks",
|
||||||
|
"aiBudgetPeriodWeekly": "Wekelijks",
|
||||||
|
"aiBudgetPeriodMonthly": "Maandelijks",
|
||||||
|
"aiBudgetPeriodYearly": "Jaarlijks",
|
||||||
|
"aiBudgetPeriodLifetime": "Levenslang",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Tokens",
|
||||||
|
"aiBudgetConflictError": "Er bestaat al een budget voor deze hersteltijd en uitgave type",
|
||||||
|
"aiBudgetInvalidAmountError": "Voer een maximale uitgave in die groter is dan 0",
|
||||||
|
"aiBudgetUpdated": "Budgetten bijgewerkt",
|
||||||
|
"aiBudgetErrorSave": "Mislukt om budgetten bij te werken",
|
||||||
|
"aiProviderType": "Type provider",
|
||||||
|
"aiProviderTypeSearch": "Zoek providers...",
|
||||||
|
"aiProviderTypeNotFound": "Geen providertype gevonden",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "Aangepast",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "Breng uw eigen eindpunt mee of route via site-doelen",
|
||||||
|
"aiProviderUpstreamUrl": "Upstream-URL",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Basis-URL voor de provider-API",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Laat leeg om de standaard upstrean URL voor deze provider te gebruiken",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Effectieve Upstream-URL",
|
||||||
|
"aiProviderApiKey": "API-sleutel",
|
||||||
|
"aiProviderApiKeyDescription": "API-sleutel gebruikt om verzoeken naar deze provider te authenticeren",
|
||||||
|
"aiProviderCustomHeadersDescription": "Headers verzonden bij elk verzoek naar deze provider. Nieuwe regel gescheiden: Header-Naam: waarde",
|
||||||
|
"aiProviderApiKeyLastChars": "API-sleutel",
|
||||||
|
"aiProviderAuthType": "Auth Type",
|
||||||
|
"aiProviderAuthTypeSearch": "Zoek auth types...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Geen auth type gevonden",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Autorisatie: Bearer-sleutel. Gebruikt door OpenAI en de meeste providers",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key header. Gebruikt door Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key header. Gebruikt door Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Autorisatie: Splunk-sleutel. Gebruikt door Splunk HTTP Event Collector",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-autorisatie: Bearer-sleutel. Gebruikt door Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "Geen Auth",
|
||||||
|
"aiProviderAuthTypePassthrough": "Passthrough",
|
||||||
|
"aiProviderAuthTypeDescription": "Hoe de upstream API verzoeken authenticeert",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Stuur de API Keys headers van de beller door naar de upstream",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Stuur geen authenticatieheaders naar de upstream",
|
||||||
|
"aiProviderRoutingMode": "Routingmodus",
|
||||||
|
"aiProviderRoutingModeDescription": "Stuur verkeer naar een upstream URL of naar HTTP-doelen op uw sites",
|
||||||
|
"aiProviderRoutingModeUrl": "Upstream-URL",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Roep een publieke of private API-basis-URL aan",
|
||||||
|
"aiProviderRoutingModeTarget": "Doelen op de site",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Routeer door doelen op uw sites",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Nadat u deze provider hebt aangemaakt, configureert u site-doelen op het tabblad Netwerkinstellingen.",
|
||||||
|
"aiProviderTargetNoOne": "Deze provider heeft geen doelen. Voeg een doel toe om verzoeken via uw sites te routeren.",
|
||||||
|
"aiProviderSkipTlsVerification": "Sla TLS-verificatie over",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Schakel TLS-certificaatverificatie voor de upstream-verbinding uit",
|
||||||
|
"aiProviderBudget": "Budget",
|
||||||
|
"aiProviderBudgetDescription": "Optioneel uitgaven- of tokenbudget voor deze provider",
|
||||||
|
"aiProviderBudgetAmount": "Budgetbedrag",
|
||||||
|
"aiProviderBudgetUnit": "Budgeteenheid",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Tokens",
|
||||||
|
"aiProviderEnabled": "Ingeschakeld",
|
||||||
|
"aiProviderEnabledDescription": "Schakel deze provider volledig uit voor alle resources",
|
||||||
|
"aiProviderErrorCreate": "Kon AI-provider niet maken",
|
||||||
|
"aiProviderErrorUpdate": "Kon de AI-provider niet bijwerken",
|
||||||
|
"aiProviderErrorDelete": "Kon AI-provider niet verwijderen",
|
||||||
|
"aiProviderErrorLoad": "Kon AI-provider niet laden",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Voer een geldige upstream-URL in",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Upstream-URL is vereist voor deze provider",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Auth type is vereist",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API-sleutel is vereist",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Site routing is alleen beschikbaar voor aangepaste providers",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Selecteer minimaal één API-mogelijkheid",
|
||||||
|
"aiProviderCapabilities": "API-mogelijkheden",
|
||||||
|
"aiProviderCapabilitiesDescription": "Selecteer welke API-formaten deze provider kan verwerken. Bekende providers beginnen met aanbevolen standaarden.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Selecteer welke API-formaten deze aangepaste provider kan verwerken",
|
||||||
|
"aiProviderCapabilitiesSelect": "Selecteer capaciteiten",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Geen capaciteiten gevonden",
|
||||||
|
"aiProviderCapabilitiesSearch": "Capaciteiten zoeken...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Chat Voltooiingen",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Ondersteunt /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI Reacties",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Ondersteunt /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic Berichten",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Ondersteunt /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini Inhoud Genereren",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Ondersteunt de directe Gemini API",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Bedrock Model Aanroep",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Ondersteunt Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex Inhoud Genereren",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Ondersteunt Vertex AI Gemini formaat",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex Rauwe Voorspelling",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Ondersteunt Vertex AI rawPredict voor Anthropic modellen",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock Conversatie",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Ondersteunt Amazon Bedrock Converse API",
|
||||||
|
"aiProviderCreated": "AI provider gemaakt",
|
||||||
|
"aiProviderUpdated": "AI provider bijgewerkt",
|
||||||
|
"aiProviderDeleted": "AI provider verwijderd",
|
||||||
|
"aiProviderDelete": "Provider verwijderen",
|
||||||
|
"aiProviderDeleteConfirm": "Provider verwijderen",
|
||||||
|
"aiProviderQuestionRemove": "Weet u zeker dat u deze AI-provider wilt verwijderen?",
|
||||||
|
"aiProviderMessageRemove": "Dit zal de provider en zijn modellen en doelen permanent verwijderen. Dit kan niet ongedaan worden gemaakt.",
|
||||||
|
"aiProviderErrorNoUpdate": "AI-provider is niet beschikbaar om te updaten",
|
||||||
|
"aiProviderModels": "Modellen",
|
||||||
|
"aiProviderModelsDescription": "Definieer toestaan- en blokkeren-lijsten voor deze provider. Verzoeken moeten overeenkomen met een toestaan-invoer en mogen niet overeenkomen met een blokkeren-invoer.",
|
||||||
|
"aiProviderCreateModelsDescription": "Kies welke modellen deze provider kan bedienen. Een lege toestaan-lijst weigert al het verkeer. U kunt later modellen toevoegen of wijzigen.",
|
||||||
|
"aiProviderModelsPlaceholder": "Zoek modellen of typ een aangepaste sleutel",
|
||||||
|
"aiProviderModelsAllow": "Toestaanlijst",
|
||||||
|
"aiProviderModelsAllowDescription": "Modellen die via deze provider mogen worden gebruikt. Leeg betekent alles weigeren.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Voer model sleutel in",
|
||||||
|
"aiProviderModelsAllowEmpty": "Nog geen modellen toegevoegd.",
|
||||||
|
"aiProviderModelsBlock": "Blokkeerlijst",
|
||||||
|
"aiProviderModelsBlockDescription": "Modellen om te weigeren, zelfs als ze overeenkomen met een toestaan-invoer.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Voer model sleutel in",
|
||||||
|
"aiProviderModelsBlockEmpty": "Nog geen modellen toegevoegd.",
|
||||||
|
"aiProviderModelsAdd": "Modellen toevoegen",
|
||||||
|
"aiProviderModelsClearAll": "Alles wissen",
|
||||||
|
"aiProviderModelsAddCustom": "Voeg \"{key}\" toe",
|
||||||
|
"aiProviderModelsAddCustomHint": "Druk op Enter om deze aangepaste modelsleutel toe te voegen.",
|
||||||
|
"aiProviderModelsAddBulk": "Voeg {count} aangepaste sleutels toe",
|
||||||
|
"aiProviderModelsAddBulkHint": "Druk op Enter om {count} aangepaste sleutels toe te voegen.",
|
||||||
|
"aiProviderModelsAddOne": "Voeg nu {key} toe",
|
||||||
|
"aiProviderModelsAddSelected": "Geselecteerde toevoegen",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} geselecteerd",
|
||||||
|
"aiProviderModelsSelectAll": "Selecteer alles",
|
||||||
|
"aiProviderModelsClearSelected": "Wissen",
|
||||||
|
"aiProviderModelsBulkHint": "Selecteer bekende modellen of typ een aangepaste sleutel.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Geen overeenkomende catalogusmodellen.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Bekende Modellen",
|
||||||
|
"aiProviderModelsAllLabel": "Alle modellen",
|
||||||
|
"aiProviderModelsAllPatternHint": "Wildcard: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Sta alle modellen toe",
|
||||||
|
"aiProviderModelsAddAllBlock": "Blokkeer alle modellen",
|
||||||
|
"aiProviderModelsAddAllDescription": "Gebruikt de * wildcard, zodat elke modelsleutel overeenkomt.",
|
||||||
|
"aiProviderModelsViewMore": "Bekijk meer ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Bekijk minder",
|
||||||
|
"aiProviderModelsRemove": "Model verwijderen",
|
||||||
|
"aiProviderModelsEditHint": "Klik om modelinstellingen te bewerken",
|
||||||
|
"aiProviderModelsSourceCatalog": "Bekend catalogusmodel",
|
||||||
|
"aiProviderModelsSourceCustom": "Aangepaste modelsleutel",
|
||||||
|
"aiProviderModelsSourcePattern": "Wildcard patroon",
|
||||||
|
"aiProviderModelsSourceAll": "Komt overeen met elke modelsleutel",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Budget geconfigureerd",
|
||||||
|
"aiProviderModelsEditTitle": "Model bewerken",
|
||||||
|
"aiProviderModelsEditDescription": "Werk de modelsleutel of budgetconfiguratie bij.",
|
||||||
|
"aiProviderModelsBudgetTab": "Budget",
|
||||||
|
"aiProviderModelsKeyLabel": "Model sleutel",
|
||||||
|
"aiProviderModelsKeyRequired": "Voer een modelsleutel in",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Deze modelsleutel staat al op een lijst",
|
||||||
|
"aiProviderModelsOverlapError": "Deze patronen kunnen niet op beide lijsten staan: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modellen bijgewerkt",
|
||||||
|
"aiProviderModelsErrorUpdate": "Modellen bijwerken mislukt",
|
||||||
|
"aiResourceProviders": "Aanbieders",
|
||||||
|
"aiResourceProvidersDescription": "Kies welke AI-providers deze AI-gateway kan gebruiken",
|
||||||
|
"aiResourceProvidersHelp": "Koppel providers en kies erven (gebruik de lijsten van elke provider) of selecteer (kies een toestaan-lijst voor deze bron). Patroonconflicten over gekoppelde providers zijn niet toegestaan.",
|
||||||
|
"aiResourceProvidersSelect": "Selecteer providers",
|
||||||
|
"aiResourceProvidersEmpty": "Geen AI-providers gevonden",
|
||||||
|
"aiResourceProvidersNoneAttached": "Nog geen providers gekoppeld.",
|
||||||
|
"aiResourceProvidersAdd": "Provider toevoegen",
|
||||||
|
"aiResourceProvidersRemove": "Provider verwijderen",
|
||||||
|
"aiResourceProviderToggleEnabled": "Schakel deze provider in of uit op de bron",
|
||||||
|
"aiResourceProviderDisabled": "Uitgeschakeld",
|
||||||
|
"aiResourceProvidersUpdated": "Providers bijgewerkt",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Providers bijwerken mislukt",
|
||||||
|
"aiResourceProviderEditDescription": "Kies hoe de modellen van deze provider op deze bron worden weergegeven.",
|
||||||
|
"viewProviderSettings": "Bekijk aanbiederinstellingen",
|
||||||
|
"aiResourceProviderMode": "Toegangsmodus",
|
||||||
|
"aiResourceProviderModeInherit": "Erf",
|
||||||
|
"aiResourceProviderModeSelect": "Selecteer",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Selecteer · {count} modellen",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Gebruik de toestaan- en blokkeren-lijsten zoals geconfigureerd op de provider.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Kies een subset van de toestaan-modellenlijst van deze provider voor deze bron.",
|
||||||
|
"aiResourceProviderAllowModels": "Toestaan lijst",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Selecteer modellen",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Zoek modellen...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Geen modellen gevonden",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Alleen modellen uit de toestaan-lijst van deze provider kunnen worden geselecteerd.",
|
||||||
|
"aiResourceAliasRequired": "Alias is vereist voor AI-gateways",
|
||||||
|
"aiResourceDomainConfiguration": "Domein Configuratie",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Kies het domein dat klanten zullen gebruiken om deze AI-gateway te bereiken.",
|
||||||
|
"aiUsageAnalyticsTitle": "AI Gebruik Analytics",
|
||||||
|
"aiUsageAnalyticsDescription": "Analyseer de kosten van de AI-gateway, tokengebruik en activiteiten over providers, bronnen, rollen en gebruikers",
|
||||||
|
"aiUsageTabOverview": "Overzicht",
|
||||||
|
"aiUsageTabProviders": "Aanbieders",
|
||||||
|
"aiUsageTabResources": "Bronnen",
|
||||||
|
"aiUsageFilterProvider": "Aanbieder",
|
||||||
|
"aiUsageFilterModel": "Model",
|
||||||
|
"aiUsageFilterResource": "Bron",
|
||||||
|
"aiUsageFilterRole": "Functie",
|
||||||
|
"aiUsageFilterUser": "Gebruiker",
|
||||||
|
"aiUsageFilterAllProviders": "Alle Providers",
|
||||||
|
"aiUsageFilterAllModels": "Alle Modellen",
|
||||||
|
"aiUsageFilterAllResources": "Alle Bronnen",
|
||||||
|
"aiUsageFilterAllRoles": "Alle Rollen",
|
||||||
|
"aiUsageFilterAllUsers": "Alle Gebruikers",
|
||||||
|
"aiUsageFilterSearch": "Zoeken...",
|
||||||
|
"aiUsageFilterNotFound": "Geen opties gevonden",
|
||||||
|
"aiUsageResetFilters": "Filters Resetten",
|
||||||
|
"aiUsageRefresh": "Vernieuwen",
|
||||||
|
"aiUsageTokenTypePrompt": "Prompt",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Cache lezen",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Cache schrijven",
|
||||||
|
"aiUsageTokenTypeCompletion": "Voltooiing",
|
||||||
|
"aiUsageTokenTypeReasoning": "Redenering",
|
||||||
|
"aiUsageRequests": "Aanvragen",
|
||||||
|
"aiUsageCost": "Kosten",
|
||||||
|
"aiUsageTokens": "Tokens",
|
||||||
|
"aiUsageOther": "Overige",
|
||||||
|
"aiUsageTotalRequests": "Totale Aanvragen",
|
||||||
|
"aiUsageTotalTokens": "Totale Tokens",
|
||||||
|
"aiUsageTotalCost": "Totale Kosten",
|
||||||
|
"aiUsageEstimated": "Geschat",
|
||||||
|
"aiUsageRequestVolume": "Aanvraagvolume",
|
||||||
|
"aiUsageTokenUsage": "Tokengebruik",
|
||||||
|
"aiUsageModelCost": "Modelkosten",
|
||||||
|
"aiUsageModelTokens": "Modeltokens",
|
||||||
|
"aiUsageTopModels": "Topmodellen",
|
||||||
|
"aiUsageTopProviders": "Topproviders",
|
||||||
|
"aiUsageProviderCost": "Providerkosten",
|
||||||
|
"aiUsageProviderTokenUsage": "Tokengebruik van de provider",
|
||||||
|
"aiUsageTopResources": "Top Bronnen",
|
||||||
|
"aiUsageResourceCost": "Bronkosten",
|
||||||
|
"aiUsageResourceTokenUsage": "Tokengebruik van de bron",
|
||||||
|
"aiUsageResourceTypePublic": "Publieke Bron",
|
||||||
|
"aiUsageResourceTypeSite": "Private Bron",
|
||||||
|
"aiUsageNoResource": "Geen bron",
|
||||||
|
"aiUsageRolesTab": "Rollen",
|
||||||
|
"aiUsageUsersTab": "Gebruikers",
|
||||||
|
"aiUsageTopRoles": "Top Rollen",
|
||||||
|
"aiUsageRoleCost": "Rol Kosten",
|
||||||
|
"aiUsageRoleTokenUsage": "Tokengebruik van de rol",
|
||||||
|
"aiUsageTopUsers": "Top Gebruikers",
|
||||||
|
"aiUsageUserCost": "Gebruikers Kosten",
|
||||||
|
"aiUsageUserTokenUsage": "Tokengebruik van de gebruiker",
|
||||||
|
"aiUsageUnknownUser": "Onbekende gebruiker",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Virtuele API-sleutels",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Virtuele API-sleutel",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Alle Virtuele API-sleutels",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Top Virtuele API-sleutels",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Kosten Virtuele API-sleuntel",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Tokengebruik van de Virtuele API-sleutel",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Geen virtuele API-sleutel",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Naamloze sleutel",
|
||||||
|
"aiUsageLoading": "Laden...",
|
||||||
|
"aiUsageNoData": "Geen data",
|
||||||
|
"resourceBudgetSettings": "Budget",
|
||||||
|
"resourceBudgetSettingsDescription": "Configureer hoe deze AI-gateway het gebruik beperkt op basis van uitgaven of tokenlimieten",
|
||||||
"sidebarApiKeys": "API sleutels",
|
"sidebarApiKeys": "API sleutels",
|
||||||
"sidebarProvisioning": "Provisie",
|
"sidebarProvisioning": "Provisie",
|
||||||
"sidebarSettings": "Instellingen",
|
"sidebarSettings": "Instellingen",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Subnet",
|
"subnetPlaceholder": "Subnet",
|
||||||
"addressDescription": "Het interne adres van de klant. Moet binnen het subnetwerk van de organisatie vallen.",
|
"addressDescription": "Het interne adres van de klant. Moet binnen het subnetwerk van de organisatie vallen.",
|
||||||
"selectSites": "Selecteer sites",
|
"selectSites": "Selecteer sites",
|
||||||
|
"selectResources": "Selecteer bronnen",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# bron} other {# bronnen}}",
|
||||||
"selectLabels": "Selecteer labels",
|
"selectLabels": "Selecteer labels",
|
||||||
"sitesDescription": "De client heeft connectiviteit met de geselecteerde sites",
|
"sitesDescription": "De client heeft connectiviteit met de geselecteerde sites",
|
||||||
"clientInstallOlm": "Installeer Olm",
|
"clientInstallOlm": "Installeer Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Tijd is in seconden",
|
"timeIsInSeconds": "Tijd is in seconden",
|
||||||
"requireDeviceApproval": "Vereist goedkeuring van apparaat",
|
"requireDeviceApproval": "Vereist goedkeuring van apparaat",
|
||||||
"requireDeviceApprovalDescription": "Gebruikers met deze rol hebben nieuwe apparaten nodig die door een beheerder zijn goedgekeurd voordat ze verbinding kunnen maken met bronnen en deze kunnen gebruiken.",
|
"requireDeviceApprovalDescription": "Gebruikers met deze rol hebben nieuwe apparaten nodig die door een beheerder zijn goedgekeurd voordat ze verbinding kunnen maken met bronnen en deze kunnen gebruiken.",
|
||||||
"sshSettings": "SSH-instellingen",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI Gateway",
|
||||||
"sshAccess": "SSH Toegang",
|
"sshAccess": "SSH Toegang",
|
||||||
"rdpSettings": "RDP-instellingen",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "VNC-instellingen",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH-server",
|
"sshServer": "SSH-server",
|
||||||
"rdpServer": "RDP-server",
|
"rdpServer": "RDP-server",
|
||||||
"vncServer": "VNC-server",
|
"vncServer": "VNC-server",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI Gateway",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Schema",
|
"editInternalResourceDialogScheme": "Schema",
|
||||||
"editInternalResourceDialogEnableSsl": "Schakel TLS in",
|
"editInternalResourceDialogEnableSsl": "Schakel TLS in",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Sta ICMP Ping toe",
|
"privateResourceAllowIcmpPing": "Sta ICMP Ping toe",
|
||||||
"privateResourceNetworkAccess": "Netwerktoegang",
|
"privateResourceNetworkAccess": "Netwerktoegang",
|
||||||
"privateResourceNetworkAccessDescription": "Beheer TCP/UDP poorttoegang en of ICMP-ping is toegestaan voor deze bron.",
|
"privateResourceNetworkAccessDescription": "Beheer TCP/UDP poorttoegang en of ICMP-ping is toegestaan voor deze bron.",
|
||||||
"hostSettings": "Hostinstellingen",
|
"hostSettings": "Host",
|
||||||
"cidrSettings": "CIDR-instellingen",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Bron-eigenschappen",
|
"createInternalResourceDialogResourceProperties": "Bron-eigenschappen",
|
||||||
"createInternalResourceDialogName": "Naam",
|
"createInternalResourceDialogName": "Naam",
|
||||||
"createInternalResourceDialogSite": "Site",
|
"createInternalResourceDialogSite": "Site",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI Gateway",
|
||||||
"scheme": "Schema",
|
"scheme": "Schema",
|
||||||
"createInternalResourceDialogScheme": "Schema",
|
"createInternalResourceDialogScheme": "Schema",
|
||||||
"createInternalResourceDialogEnableSsl": "Schakel TLS in",
|
"createInternalResourceDialogEnableSsl": "Schakel TLS in",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Geldig wachtwoord",
|
"validPassword": "Geldig wachtwoord",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Geldige Virtuele API-sleutel",
|
||||||
"view": "Bekijk",
|
"view": "Bekijk",
|
||||||
"configManaged": "Configuratie Beheerd",
|
"configManaged": "Configuratie Beheerd",
|
||||||
"connectedClient": "Verbonden Client",
|
"connectedClient": "Verbonden Client",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Log bewaring",
|
"logRetention": "Log bewaring",
|
||||||
"logRetentionDescription": "Beheren hoe lang verschillende soorten logs bewaard worden voor deze organisatie of schakel ze uit",
|
"logRetentionDescription": "Beheren hoe lang verschillende soorten logs bewaard worden voor deze organisatie of schakel ze uit",
|
||||||
"requestLogsDescription": "Bekijk gedetailleerde verzoeklogboeken voor resources in deze organisatie",
|
"requestLogsDescription": "Bekijk gedetailleerde verzoeklogboeken voor resources in deze organisatie",
|
||||||
|
"aiSessionLogs": "AI Gateway Sessie Logs",
|
||||||
|
"aiSessionLogsDescription": "Bekijk prompt- en reactie-transcripten voor AI-gateway-aanvragen in deze organisatie",
|
||||||
|
"sidebarLogsAi": "Sessie Logs",
|
||||||
|
"commandLogsAi": "Sessie Logs",
|
||||||
|
"sidebarLogsAiUsage": "Gebruiksanalyses",
|
||||||
|
"commandLogsAiUsage": "Gebruiksanalyses",
|
||||||
|
"provider": "Aanbieder",
|
||||||
|
"capability": "Capaciteit",
|
||||||
|
"model": "Model",
|
||||||
|
"virtualApiKey": "Virtuele API-sleutel",
|
||||||
|
"noVirtualApiKey": "Geen virtuele API-sleutel",
|
||||||
|
"stream": "Stroom",
|
||||||
|
"streaming": "Streamen",
|
||||||
|
"nonStreaming": "Niet-streamen",
|
||||||
|
"statusCode": "Statuscode",
|
||||||
|
"aiSessionId": "Sessie-ID",
|
||||||
|
"aiSessionRequest": "Verzoek",
|
||||||
|
"aiSessionResponse": "Antwoord",
|
||||||
|
"aiSessionNoData": "Geen gegevens vastgelegd",
|
||||||
|
"aiSessionCouldNotParse": "(ruwe, kan transcript niet parseren)",
|
||||||
|
"aiSessionLogTruncated": "Deze sessie is ingekort voordat deze werd opgeslagen en kan onvolledig zijn.",
|
||||||
|
"aiSessionViewRaw": "Bekijk Ruwe JSON",
|
||||||
|
"aiSessionViewChat": "Bekijk Chat",
|
||||||
|
"cost": "Kosten",
|
||||||
|
"estimated": "Geschat",
|
||||||
|
"tokenUsage": "Token Gebruiken",
|
||||||
|
"promptTokens": "Prompttokens",
|
||||||
|
"cacheReadTokens": "Cache Lees Tokens",
|
||||||
|
"cacheWriteTokens": "Cache Schrijf Tokens",
|
||||||
|
"completionTokens": "Voltooiings Tokens",
|
||||||
|
"reasoningTokens": "Redenerings Tokens",
|
||||||
|
"totalTokens": "Totale Tokens",
|
||||||
"requestAnalyticsDescription": "Bekijk gedetailleerde request analytics voor resources in deze organisatie",
|
"requestAnalyticsDescription": "Bekijk gedetailleerde request analytics voor resources in deze organisatie",
|
||||||
"logRetentionRequestLabel": "Bewaring van HTTP-aanvraaglogboeken",
|
"logRetentionRequestLabel": "Bewaring van HTTP-aanvraaglogboeken",
|
||||||
"logRetentionRequestDescription": "Hoe lang de aanvraaglogboeken te behouden",
|
"logRetentionRequestDescription": "Hoe lang de aanvraaglogboeken te behouden",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Hoe lang de action logs behouden moeten blijven",
|
"logRetentionActionDescription": "Hoe lang de action logs behouden moeten blijven",
|
||||||
"logRetentionConnectionLabel": "Connectie log bewaring",
|
"logRetentionConnectionLabel": "Connectie log bewaring",
|
||||||
"logRetentionConnectionDescription": "Hoe lang de verbindingslogs onderhouden",
|
"logRetentionConnectionDescription": "Hoe lang de verbindingslogs onderhouden",
|
||||||
|
"logRetentionAISessionsLabel": "AI Gateway Sessie Log Retentie",
|
||||||
|
"logRetentionAISessionsDescription": "Hoe lang AI-gateway prompt/response sessielogs te behouden",
|
||||||
"logRetentionDisabled": "Uitgeschakeld",
|
"logRetentionDisabled": "Uitgeschakeld",
|
||||||
"logRetention3Days": "3 dagen",
|
"logRetention3Days": "3 dagen",
|
||||||
"logRetention7Days": "7 dagen",
|
"logRetention7Days": "7 dagen",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Bron adres",
|
"sourceAddress": "Bron adres",
|
||||||
"destinationAddress": "Adres bestemming",
|
"destinationAddress": "Adres bestemming",
|
||||||
"duration": "Duur",
|
"duration": "Duur",
|
||||||
"licenseRequiredToUse": "Een <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> licentie of <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is vereist om deze functie te gebruiken. <bookADemoLink>Boek een gratis demo of POC trial om meer te leren.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition licentie</enterpriseEditionLink> is vereist. <bookADemoLink>Boek een demo of proefversie</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "De <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Boek een gratis demo of POC trial om meer te leren.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition licentie</enterpriseEditionLink> is vereist. <bookADemoLink>Boek een demo of proefversie</bookADemoLink>",
|
||||||
"certResolver": "Certificaat Resolver",
|
"certResolver": "Certificaat Resolver",
|
||||||
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
|
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
|
||||||
"selectCertResolver": "Certificaat Resolver selecteren",
|
"selectCertResolver": "Certificaat Resolver selecteren",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Selecteren van meerdere sites maakt veerkrachtige routing en failover mogelijk voor hoge beschikbaarheid.",
|
"internalResourceFormMultiSiteRoutingHelp": "Selecteren van meerdere sites maakt veerkrachtige routing en failover mogelijk voor hoge beschikbaarheid.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Meer informatie",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Meer informatie",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Beperk toegang tot specifieke TCP/UDP-poorten of sta alle poorten toe/blokkeer.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Beperk toegang tot specifieke TCP/UDP-poorten of sta alle poorten toe/blokkeer.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "HTTP-configuratie",
|
"createInternalResourceDialogHttpConfiguration": "Domein Configuratie",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Kies het domein dat cliënten zullen gebruiken om deze bron via HTTP of HTTPS te bereiken.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Kies het domein dat cliënten zullen gebruiken om deze bron via HTTP of HTTPS te bereiken.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "HTTP-configuratie",
|
"editInternalResourceDialogHttpConfiguration": "Domein Configuratie",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Kies het domein dat cliënten zullen gebruiken om deze bron via HTTP of HTTPS te bereiken.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Kies het domein dat cliënten zullen gebruiken om deze bron via HTTP of HTTPS te bereiken.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Geen label",
|
"resourceLauncherUnlabeled": "Geen label",
|
||||||
|
"resourceLauncherAiGateway": "AI Gateway",
|
||||||
"resourceLauncherNoSite": "Geen Site",
|
"resourceLauncherNoSite": "Geen Site",
|
||||||
|
"resourceLauncherAvailableModels": "Beschikbare Modellen",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modellen die u met deze AI-gateway kunt gebruiken.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Er zijn geen modellen beschikbaar voor deze bron.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Kon beschikbare modellen niet laden.",
|
||||||
|
"resourceLauncherApiKeys": "API Sleutels",
|
||||||
|
"resourceLauncherApiKeysDescription": "Gebruik uw identiteitsleutel of een toegeschreven sleutel om authenticatie te verlenen met deze bron.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Identiteits Sleutel",
|
||||||
|
"resourceLauncherApiKeysManual": "Toegeschreven Sleutels",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Er zijn geen API-sleutels beschikbaar voor deze bron.",
|
||||||
|
"resourceLauncherApiKeysError": "Kon API-sleutels niet laden.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Geen bronnen in deze groep",
|
"resourceLauncherNoResourcesInGroup": "Geen bronnen in deze groep",
|
||||||
"resourceLauncherEmptyStateTitle": "Geen Bronnen Beschikbaar",
|
"resourceLauncherEmptyStateTitle": "Geen Bronnen Beschikbaar",
|
||||||
"resourceLauncherEmptyStateDescription": "Je hebt nog geen toegang tot bronnen. Neem contact op met je beheerder om toegang aan te vragen.",
|
"resourceLauncherEmptyStateDescription": "Je hebt nog geen toegang tot bronnen. Neem contact op met je beheerder om toegang aan te vragen.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Kon de launcher-weergave niet verwijderen. Probeer het opnieuw.",
|
"resourceLauncherViewDeleteFailedDescription": "Kon de launcher-weergave niet verwijderen. Probeer het opnieuw.",
|
||||||
"memberPortalPrevious": "Vorige",
|
"memberPortalPrevious": "Vorige",
|
||||||
"memberPortalNext": "Volgende",
|
"memberPortalNext": "Volgende",
|
||||||
"httpSettings": "HTTP-instellingen",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "TCP-instellingen",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "UDP-instellingen",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Veilige verbinding tot stand brengen…",
|
"sshConnectingDescription": "Veilige verbinding tot stand brengen…",
|
||||||
"sshConnecting": "Verbinding maken…",
|
"sshConnecting": "Verbinding maken…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Upewnij się, że jesteś zalogowany jako właściwy użytkownik.",
|
"inviteLoginUser": "Upewnij się, że jesteś zalogowany jako właściwy użytkownik.",
|
||||||
"inviteErrorNoUser": "Przykro nam, ale wygląda na to, że zaproszenie, do którego próbujesz uzyskać dostęp, nie jest dla użytkownika, który istnieje.",
|
"inviteErrorNoUser": "Przykro nam, ale wygląda na to, że zaproszenie, do którego próbujesz uzyskać dostęp, nie jest dla użytkownika, który istnieje.",
|
||||||
"inviteCreateUser": "Proszę najpierw utworzyć konto.",
|
"inviteCreateUser": "Proszę najpierw utworzyć konto.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Zaproszenia mogą być akceptowane tylko przez wewnętrzne konta. Wyloguj się i zaloguj, używając hasła dla tego e-maila.",
|
||||||
|
"inviteLoginInternalOnly": "Zaproszenia wymagają wewnętrznego konta z hasłem. Stwórz konto lub zaloguj się używając hasła.",
|
||||||
"goHome": "Przejdź do strony głównej",
|
"goHome": "Przejdź do strony głównej",
|
||||||
"inviteLogInOtherUser": "Zaloguj się jako inny użytkownik",
|
"inviteLogInOtherUser": "Zaloguj się jako inny użytkownik",
|
||||||
"createAnAccount": "Utwórz konto",
|
"createAnAccount": "Utwórz konto",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Nie znaleziono kraju.",
|
"noCountryFound": "Nie znaleziono kraju.",
|
||||||
"siteSelectionDescription": "Ta strona zapewni połączenie z celem.",
|
"siteSelectionDescription": "Ta strona zapewni połączenie z celem.",
|
||||||
"resourceType": "Typ zasobu",
|
"resourceType": "Typ zasobu",
|
||||||
"resourceTypeDescription": "To kontroluje protokół zasobu i sposób jego renderowania w przeglądarce. Później nie można tego zmienić.",
|
"resourceTypeDescription": "To kontroluje protokół zasobów i nie można tego później zmienić.",
|
||||||
|
"resourceTypeSearch": "Szukaj typów zasobów...",
|
||||||
|
"resourceTypeNotFound": "Nie znaleziono typu zasobu",
|
||||||
|
"resourceTypeHttpDescription": "Proxy ruchu sieciowego przez HTTPS za pomocą nazwy domeny",
|
||||||
|
"resourceTypeInferenceDescription": "Przekieruj żądania AI przez dołączonych dostawców",
|
||||||
|
"resourceTypeSshDescription": "Dostęp do serwera SSH z przeglądarki",
|
||||||
|
"resourceTypeRdpDescription": "Dostęp do pulpitu zdalnego z przeglądarki",
|
||||||
|
"resourceTypeVncDescription": "Dostęp do pulpitu VNC z przeglądarki",
|
||||||
|
"resourceTypeTcpDescription": "Proxy surowego ruchu TCP za pomocą numeru portu",
|
||||||
|
"resourceTypeUdpDescription": "Proxy surowego ruchu UDP za pomocą numeru portu",
|
||||||
|
"privateResourceTypeDescription": "To kontroluje, jak klienci uzyskują dostęp do zasobu. To nie może być zmienione później.",
|
||||||
|
"privateResourceTypeHostDescription": "Udostępnij pojedynczego hosta w sieci witryny dla połączonych klientów",
|
||||||
|
"privateResourceTypeCidrDescription": "Udostępnij zakres CIDR w sieci witryny dla połączonych klientów",
|
||||||
|
"privateResourceTypeHttpDescription": "Dostęp do usługi HTTP lub HTTPS przez domenę",
|
||||||
|
"privateResourceTypeSshDescription": "Dostęp do serwera SSH dla połączonych klientów",
|
||||||
"resourceDomainDescription": "Zasób będzie udostępniany pod tym w pełni kwalifikowanym adresem domenowym.",
|
"resourceDomainDescription": "Zasób będzie udostępniany pod tym w pełni kwalifikowanym adresem domenowym.",
|
||||||
"resourceHTTPSSettings": "Ustawienia HTTPS",
|
"resourceHTTPSSettings": "Ustawienia HTTPS",
|
||||||
"resourceHTTPSSettingsDescription": "Skonfiguruj jak zasób będzie dostępny przez HTTPS",
|
"resourceHTTPSSettingsDescription": "Skonfiguruj jak zasób będzie dostępny przez HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Zapisz ustawienia",
|
"authMethodsSave": "Zapisz ustawienia",
|
||||||
"policyAuthStackTitle": "Uwierzytelnianie",
|
"policyAuthStackTitle": "Uwierzytelnianie",
|
||||||
"policyAuthStackDescription": "Kontroluj, które metody uwierzytelniania są wymagane do uzyskania dostępu do tego zasobu",
|
"policyAuthStackDescription": "Kontroluj, które metody uwierzytelniania są wymagane do uzyskania dostępu do tego zasobu",
|
||||||
|
"policyAuthInferenceStackDescription": "Wybierz, którzy użytkownicy i role mogą się uwierzytelniać w tym stogu inferencyjnym AI",
|
||||||
"policyAuthOrLogicTitle": "Kilka metod uwierzytelniania jest aktywnych",
|
"policyAuthOrLogicTitle": "Kilka metod uwierzytelniania jest aktywnych",
|
||||||
"policyAuthOrLogicBanner": "Odwiedzający mogą się uwierzytelnić, korzystając z jednej z poniższych aktywnych metod. Nie muszą ukończyć wszystkich z nich.",
|
"policyAuthOrLogicBanner": "Odwiedzający mogą się uwierzytelnić, korzystając z jednej z poniższych aktywnych metod. Nie muszą ukończyć wszystkich z nich.",
|
||||||
"policyAuthMethodActive": "Aktywny",
|
"policyAuthMethodActive": "Aktywny",
|
||||||
"policyAuthMethodOff": "Wyłączony",
|
"policyAuthMethodOff": "Wyłączony",
|
||||||
"policyAuthSsoTitle": "Platforma SSO",
|
"policyAuthSsoTitle": "Platforma SSO",
|
||||||
"policyAuthSsoDescription": "Wymagany znak w identyfikatorze dostawcy Twojej organizacji",
|
"policyAuthSsoDescription": "Wymagany znak w identyfikatorze dostawcy Twojej organizacji",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "URL logowania",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Każdy użytkownik już posiada klucz API tożsamości, więc musisz tylko utworzyć wirtualne klucze API dla klientów niebędących użytkownikami lub wspólnego dostępu. Użytkownicy mogą pobrać swój klucz, logując się przez swojego dostawcę tożsamości na URL tego zasobu, gdzie będzie pokazany po zalogowaniu.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Każdy użytkownik już posiada klucz API tożsamości, więc musisz tylko utworzyć wirtualne klucze API dla klientów niebędących użytkownikami lub wspólnego dostępu. Użytkownicy pobierają swój klucz tutaj po zalogowaniu się przez swojego dostawcę tożsamości.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} użytkowników, {roles} ról",
|
"policyAuthSsoSummary": "{idp} · {users} użytkowników, {roles} ról",
|
||||||
"policyAuthSsoDefaultIdp": "Dostawca domyślny",
|
"policyAuthSsoDefaultIdp": "Dostawca domyślny",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Dodaj Dostawcę Tożsamości Domyślnej",
|
"policyAuthAddDefaultIdentityProvider": "Dodaj Dostawcę Tożsamości Domyślnej",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Nowa rola jest wymagana",
|
"accessRoleErrorNewRequired": "Nowa rola jest wymagana",
|
||||||
"accessRoleErrorRemove": "Nie udało się usunąć roli",
|
"accessRoleErrorRemove": "Nie udało się usunąć roli",
|
||||||
"accessRoleErrorRemoveDescription": "Wystąpił błąd podczas usuwania roli.",
|
"accessRoleErrorRemoveDescription": "Wystąpił błąd podczas usuwania roli.",
|
||||||
|
"accessRoleInferenceBudget": "Budżet AI",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Skonfiguruj, jak członkowie tej roli ograniczają użycie AI na podstawie wydatków lub limitów tokenów",
|
||||||
"accessRoleName": "Nazwa roli",
|
"accessRoleName": "Nazwa roli",
|
||||||
"accessRoleQuestionRemove": "Zamierzasz usunąć rolę `{name}`. Nie możesz cofnąć tej czynności.",
|
"accessRoleQuestionRemove": "Zamierzasz usunąć rolę `{name}`. Nie możesz cofnąć tej czynności.",
|
||||||
"accessRoleRemove": "Usuń rolę",
|
"accessRoleRemove": "Usuń rolę",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Usuń witrynę",
|
"actionDeleteSite": "Usuń witrynę",
|
||||||
"actionGetSite": "Pobierz witrynę",
|
"actionGetSite": "Pobierz witrynę",
|
||||||
"actionListSites": "Lista witryn",
|
"actionListSites": "Lista witryn",
|
||||||
|
"actionCreateAiProvider": "Utwórz Dostawcę AI",
|
||||||
|
"actionDeleteAiProvider": "Usuń Dostawcę AI",
|
||||||
|
"actionGetAiProvider": "Pobierz Dostawcę AI",
|
||||||
|
"actionListAiProviders": "Lista Dostawców AI",
|
||||||
|
"actionUpdateAiProvider": "Aktualizuj Dostawcę AI",
|
||||||
|
"actionCreateAiModel": "Utwórz Model AI",
|
||||||
|
"actionDeleteAiModel": "Usuń Model AI",
|
||||||
|
"actionGetAiModel": "Pobierz Model AI",
|
||||||
|
"actionListAiModels": "Lista Modeli AI",
|
||||||
|
"actionUpdateAiModel": "Aktualizuj Model AI",
|
||||||
|
"actionCreateAiBudget": "Utwórz Budżet AI",
|
||||||
|
"actionDeleteAiBudget": "Usuń Budżet AI",
|
||||||
|
"actionGetAiBudget": "Pobierz Budżet AI",
|
||||||
|
"actionListAiBudgets": "Lista Budżetów AI",
|
||||||
|
"actionUpdateAiBudget": "Aktualizuj Budżet AI",
|
||||||
|
"actionListResourceAiModels": "Lista modeli AI dla zasobu",
|
||||||
|
"actionSetResourceAiModels": "Ustaw modele AI dla zasobu",
|
||||||
|
"actionCreateVirtualApiKey": "Utwórz Wirtualny Klucz API",
|
||||||
|
"actionDeleteVirtualApiKey": "Usuń Wirtualny Klucz API",
|
||||||
|
"actionGetVirtualApiKey": "Pobierz Wirtualny Klucz API",
|
||||||
|
"actionListVirtualApiKeys": "Lista Wirtualnych Kluczy API",
|
||||||
|
"actionUpdateVirtualApiKey": "Aktualizuj Wirtualny Klucz API",
|
||||||
"actionApplyBlueprint": "Zastosuj schemat",
|
"actionApplyBlueprint": "Zastosuj schemat",
|
||||||
"actionListBlueprints": "Lista planów",
|
"actionListBlueprints": "Lista planów",
|
||||||
"actionGetBlueprint": "Pobierz plan",
|
"actionGetBlueprint": "Pobierz plan",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Ustaw kod PIN zasobu",
|
"actionSetResourcePincode": "Ustaw kod PIN zasobu",
|
||||||
"actionSetResourceEmailWhitelist": "Ustaw białą listę email zasobu",
|
"actionSetResourceEmailWhitelist": "Ustaw białą listę email zasobu",
|
||||||
"actionGetResourceEmailWhitelist": "Pobierz białą listę email zasobu",
|
"actionGetResourceEmailWhitelist": "Pobierz białą listę email zasobu",
|
||||||
|
"actionListResourcePolicies": "Lista zasad zasobu",
|
||||||
|
"actionCreateResourcePolicy": "Utwórz zasadę dla zasobu",
|
||||||
"actionGetResourcePolicy": "Pobierz politykę zasobów",
|
"actionGetResourcePolicy": "Pobierz politykę zasobów",
|
||||||
"actionUpdateResourcePolicy": "Zaktualizuj politykę zasobów",
|
"actionUpdateResourcePolicy": "Zaktualizuj politykę zasobów",
|
||||||
|
"actionDeleteResourcePolicy": "Usuń zasadę dla zasobu",
|
||||||
"actionSetResourcePolicyUsers": "Ustaw użytkowników polityki zasobów",
|
"actionSetResourcePolicyUsers": "Ustaw użytkowników polityki zasobów",
|
||||||
"actionSetResourcePolicyRoles": "Ustaw role polityki zasobów",
|
"actionSetResourcePolicyRoles": "Ustaw role polityki zasobów",
|
||||||
"actionSetResourcePolicyPassword": "Ustaw hasło polityki zasobów",
|
"actionSetResourcePolicyPassword": "Ustaw hasło polityki zasobów",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Zaproszenia",
|
"sidebarInvitations": "Zaproszenia",
|
||||||
"sidebarRoles": "Role",
|
"sidebarRoles": "Role",
|
||||||
"sidebarShareableLinks": "Linki do udostępnienia",
|
"sidebarShareableLinks": "Linki do udostępnienia",
|
||||||
|
"sidebarAiGateway": "Gateway AI",
|
||||||
|
"sidebarAiProviders": "Dostawcy",
|
||||||
|
"commandAiProviders": "Dostawcy AI",
|
||||||
|
"sidebarVirtualApiKeys": "Wirtualne Klucze API",
|
||||||
|
"sidebarMyApiKeys": "Twoje Klucze API",
|
||||||
|
"sidebarAccount": "Launcher",
|
||||||
|
"commandVirtualApiKeys": "Wirtualne Klucze API",
|
||||||
|
"virtualApiKeysTitle": "Zarządzaj Wirtualnymi Kluczami API",
|
||||||
|
"virtualApiKeysDescription": "Utwórz i zarządzaj ręcznymi kluczami API dla dostępu do publicznych bram AI",
|
||||||
|
"virtualApiKeysTabIdentity": "Klucze Tożsamości",
|
||||||
|
"virtualApiKeysTabVirtual": "Klucze Wirtualne",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Klucze Tożsamości dla Każdego Użytkownika",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Każdy użytkownik już posiada klucz tożsamości Pangolin dla tej organizacji. Jest unikalny dla ich konta i uwierzytelnia ich do bramek AI, do których mogą uzyskać dostęp.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Przykład",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Jak Użytkownicy Pozyskują Swoje Klucze",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Odwiedź publiczny URL Gateway AI w przeglądarce i zaloguj się na swoje konto.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Lub odwiedź <url></url> podczas zalogowania się.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Możesz stworzyć dodatkowe wirtualne klucze API na karcie Klucze Wirtualne. Te klucze mogą być przypisane do konkretnych publicznych bram AI i opcjonalnie przypisane do użytkownika. Tworzenie klucza natychmiast przyznaje dostęp do wybranych publicznych bramek AI.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Ręcznie Utwórz Klucz",
|
||||||
|
"virtualApiKeysEmailIdentity": "Wyślij Klucze Tożsamości Emailem",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Wyślij każdemu z wybranych użytkownikowi lub roli ich klucz tożsamości Pangolin.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Wyślij do wszystkich użytkowników",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Wyślij do wszystkich członków organizacji, którzy mają emaile kont.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Użytkownicy",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Role",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Wyślij Emaile",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Klucze tożsamości wysłane emailem",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Wysłano {sent} wiadomości e-mail.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} użytkowników zostało pominiętych, ponieważ nie mają adresu e-mail.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Wybierz co najmniej jednego użytkownika lub rolę, lub wyślij do wszystkich użytkowników.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Błąd przy wysyłaniu kluczy tożsamości",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Nie udało się wysłać emaila z kluczami tożsamości",
|
||||||
|
"virtualApiKeysBannerTitle": "Klucze Tożsamości dla Każdego Użytkownika",
|
||||||
|
"virtualApiKeysBannerDescription": "Każdy użytkownik już posiada klucz tożsamości dostępny pod {keysUrl}. Możesz również ręcznie wygenerować klucze tutaj, które przyznają bezpośredni dostęp do publicznych bramek AI.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Zobacz Klucze Tożsamości",
|
||||||
|
"virtualApiKeys": "Wirtualne Klucze API",
|
||||||
|
"virtualApiKeysSearch": "Szukaj kluczy...",
|
||||||
|
"virtualApiKeysCreate": "Utwórz Wirtualny Klucz API",
|
||||||
|
"virtualApiKeysCreateDescription": "Uwierz klucz ręczny, który może wywołać publiczne bramki AI w tej organizacji",
|
||||||
|
"virtualApiKeysCreateButton": "Utwórz Klucz",
|
||||||
|
"virtualApiKeysEmpty": "Nie ma jeszcze wirtualnych kluczy API",
|
||||||
|
"virtualApiKeysName": "Nazwa",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Opis (opcjonalnie)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Przypisz Użytkownika (opcjonalnie)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Przypisz ten klucz do użytkownika, aby śledzić użycie. Po utworzeniu klucz natychmiast przyznaje dostęp do wybranych publicznych bramek AI. Nie musisz przypisywać użytkownika ręcznie do zasobu. Klucz pojawi się również w profilu użytkownika.",
|
||||||
|
"virtualApiKeysAllResources": "Wszystkie publiczne bramki AI",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Pozwól temu kluczowi uzyskać dostęp do każdej publicznej bramki AI w organizacji",
|
||||||
|
"virtualApiKeysSelectResources": "Publiczne Bramki AI",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Wybierz zasoby",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Wybierz, do których publicznych bramek AI ten klucz może uzyskać dostęp",
|
||||||
|
"virtualApiKeysNoResources": "Brak zasobów",
|
||||||
|
"virtualApiKeysSecret": "Klucz",
|
||||||
|
"virtualApiKeysCopyKey": "Skopiuj ten klucz. Możesz go ponownie zobaczyć później w tabeli lub podczas edytowania.",
|
||||||
|
"virtualApiKeysViewSecret": "Zobacz Sekret",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Sekret Wirtualnego Klucza API",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Ten sekret zapewnia dostęp do publicznych bramek AI przypisanych do tego klucza",
|
||||||
|
"virtualApiKeysEdit": "Edytuj Wirtualny Klucz API",
|
||||||
|
"virtualApiKeysEditDescription": "Zaktualizuj powiązanego użytkownika i dostęp do publicznych bramek AI dla tego klucza",
|
||||||
|
"virtualApiKeysSaveButton": "Zapisz zmiany",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Wybierz co najmniej jedną publiczną bramkę AI lub włącz wszystkie publiczne bramki AI",
|
||||||
|
"virtualApiKeysUpdated": "Wirtualny klucz API zaktualizowany",
|
||||||
|
"virtualApiKeysUpdatedDescription": "Wirtualny klucz API został zaktualizowany",
|
||||||
|
"virtualApiKeysErrorUpdate": "Błąd podczas aktualizacji wirtualnego klucza API",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Nie udało się zaktualizować wirtualnego klucza API",
|
||||||
|
"virtualApiKeysErrorCreate": "Błąd podczas tworzenia wirtualnego klucza API",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Nie udało się utworzyć wirtualnego klucza API",
|
||||||
|
"virtualApiKeysErrorDelete": "Błąd podczas usuwania wirtualnego klucza API",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Nie udało się usunąć wirtualnego klucza API",
|
||||||
|
"virtualApiKeysDeleted": "Wirtualny klucz API usunięty",
|
||||||
|
"virtualApiKeysDeletedDescription": "Wirtualny klucz API został usunięty",
|
||||||
|
"virtualApiKeysDelete": "Usuń Wirtualny Klucz API",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Usuń Klucz",
|
||||||
|
"virtualApiKeysQuestionRemove": "Czy na pewno chcesz usunąć ten wirtualny klucz API?",
|
||||||
|
"virtualApiKeysMessageRemove": "Klienci korzystający z tego klucza stracą natychmiastowo dostęp.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Błąd ładowania sekretu",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Nie udało się załadować sekretu wirtualnego klucza API",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Nieprzypisane",
|
||||||
|
"virtualApiKeysInferenceBudget": "Budżet",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Skonfiguruj, jak ten klucz ogranicza użytkowanie AI na podstawie wydatków lub limitów tokenów",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Wyślij klucz po jego wygenerowaniu",
|
||||||
|
"virtualApiKeysEmailThisKey": "Wyślij ten klucz emailem",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Wyślij klucz do powiązanego użytkownika i dodatkowych adresów po jego utworzeniu",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Wyślij bieżący klucz do powiązanego użytkownika i dodatkowych adresów",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "E-maile nie są skonfigurowane na tym serwerze",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Skonfiguruj SMTP, aby wysłać wirtualne klucze API emailem.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Wyślij do powiązanego użytkownika",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Wyślij klucz na e-mail konta powiązanego użytkownika",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Przypisz użytkownika, aby wysłać mu klucz",
|
||||||
|
"virtualApiKeysEmailAdditional": "Dodatkowe emaile",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Dodaj email i naciśnij Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Wybierz powiązanego użytkownika lub dodaj co najmniej jeden adres e-mail",
|
||||||
|
"myVirtualApiKeysTitle": "Twoje Klucze API",
|
||||||
|
"myVirtualApiKeysDescription": "Zobacz swój klucz tożsamości i wszelkie wirtualne klucze API przypisane do Ciebie w tej organizacji",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Twoje Klucze API dla {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Zobacz swój klucz tożsamości i wirtualne klucze API przypisane do Ciebie, które mogą uzyskać dostęp do {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Klucz Tożsamości",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Twój Osobisty Klucz API",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Twój osobisty klucz dla tej organizacji. Jest unikalny dla twojego konta i używany, aby zidentyfikować cię podczas wywoływania zasobów Gateway AI.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Twój Osobisty Klucz API dla {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Twój osobisty klucz dla tej organizacji. Użyj go, aby wywołać {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Przypisane Klucze",
|
||||||
|
"myVirtualApiKeysManualDescription": "Ręczne wirtualne klucze API, które administrator przypisał do twojego konta",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Ręczne wirtualne klucze API przypisane do twojego konta, które mogą uzyskać dostęp do {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Nie ma jeszcze przypisanych kluczy",
|
||||||
|
"myVirtualApiKeysKindUser": "Tożsamość",
|
||||||
|
"myVirtualApiKeysKindManual": "Ręczne",
|
||||||
|
"myVirtualApiKeysUnnamed": "Bez nazwy",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Ujawnij Sekret",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Ten sekret uwierzytelnia cię do zasobów Gateway AI",
|
||||||
|
"aiProvidersTitle": "Dostawcy AI",
|
||||||
|
"aiProvidersDescription": "Połącz dostawców modeli dla obciążeń AI w tej organizacji",
|
||||||
|
"aiProvidersBannerTitle": "Połącz dostawców modeli",
|
||||||
|
"aiProvidersBannerDescription": "Dostawcy są modelami backendowymi, które Pangolin używa dla obciążeń AI. Połącz OpenAI, Anthropic i innych dostawców tutaj, a następnie dołącz je do publicznych bramek AI, aby użytkownicy mogli wywoływać modele z dostępem świadomym tożsamości, budżetami i logowaniem.",
|
||||||
|
"aiProvidersAdd": "Dodaj Dostawcę",
|
||||||
|
"aiProvidersSearch": "Szukaj dostawców...",
|
||||||
|
"aiProvidersEmpty": "Nie ma jeszcze dostawców AI",
|
||||||
|
"aiProviderCreate": "Utwórz Dostawcę AI",
|
||||||
|
"aiProviderCreateDescription": "Dodaj dostawcę modeli dla tej organizacji",
|
||||||
|
"aiProviderSeeAll": "Zobacz Wszystkich Dostawców",
|
||||||
|
"aiProviderSetting": "Ustawienia dostawcy dla {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Skonfiguruj tego dostawcę AI",
|
||||||
|
"aiProviderGeneral": "Ogólne",
|
||||||
|
"aiProviderGeneralDescription": "Podstawowe ustawienia dla tego dostawcy",
|
||||||
|
"aiProviderConfiguration": "Konfiguracja",
|
||||||
|
"aiProviderConfigurationDescription": "Routing sieciowy i uwierzytelnianie dla tego dostawcy",
|
||||||
|
"aiProviderNetworkSettings": "Ustawienia sieciowe",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Wybierz, jak ruch dociera do tego dostawcy",
|
||||||
|
"aiProviderAuthSettings": "Uwierzytelnianie",
|
||||||
|
"aiProviderAuthSettingsDescription": "Skonfiguruj, jak ten dostawca uwierzytelnia zapytania do swojego URL upstream",
|
||||||
|
"aiProviderBudgetSettings": "Budżet",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Skonfiguruj, jak ten dostawca ogranicza użycie na podstawie wydatków lub limitów tokenów",
|
||||||
|
"aiBudgetAdd": "Dodaj Budżet",
|
||||||
|
"aiBudgetEmpty": "Nie skonfigurowano jeszcze budżetów. Kliknij Dodaj Budżet, aby ustawić limit wydatków lub tokenów.",
|
||||||
|
"aiBudgetUnit": "Typ wydatku",
|
||||||
|
"aiBudgetPeriod": "Okres resetowania",
|
||||||
|
"aiBudgetAmount": "Maksymalny Wydatek",
|
||||||
|
"aiBudgetAmountPlaceholder": "Maksymalny wydatek",
|
||||||
|
"aiBudgetPeriodHourly": "Co godzinę",
|
||||||
|
"aiBudgetPeriodDaily": "Codziennie",
|
||||||
|
"aiBudgetPeriodWeekly": "Cotygodniowo",
|
||||||
|
"aiBudgetPeriodMonthly": "Co miesiąc",
|
||||||
|
"aiBudgetPeriodYearly": "Co rok",
|
||||||
|
"aiBudgetPeriodLifetime": "Na całe życie",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Tokeny",
|
||||||
|
"aiBudgetConflictError": "Istnieje już budżet dla tego okresu resetowania i typu wydatku",
|
||||||
|
"aiBudgetInvalidAmountError": "Wprowadź maksymalny wydatek większy niż 0",
|
||||||
|
"aiBudgetUpdated": "Budżety zaktualizowane",
|
||||||
|
"aiBudgetErrorSave": "Nie udało się zaktualizować budżetów",
|
||||||
|
"aiProviderType": "Typ dostawcy",
|
||||||
|
"aiProviderTypeSearch": "Szukaj dostawców...",
|
||||||
|
"aiProviderTypeNotFound": "Nie znaleziono typu dostawcy",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "Niestandardowy",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "Przynieś swój własny endpoint lub trasę przez cele witryny",
|
||||||
|
"aiProviderUpstreamUrl": "URL do góry",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Podstawowy URL dla API dostawcy",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Pozostaw puste, aby użyć domyślnego URL-u w górnym kierunku dla tego dostawcy",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Efektywny URL do góry",
|
||||||
|
"aiProviderApiKey": "Klucz API",
|
||||||
|
"aiProviderApiKeyDescription": "Klucz API używany do uwierzytelniania zapytań do tego dostawcy",
|
||||||
|
"aiProviderCustomHeadersDescription": "Nagłówki wysyłane przy każdym zapytaniu do tego dostawcy. Oddzielone nową linią: Nazwa nagłówka: wartość",
|
||||||
|
"aiProviderApiKeyLastChars": "Klucz API",
|
||||||
|
"aiProviderAuthType": "Typ autoryzacji",
|
||||||
|
"aiProviderAuthTypeSearch": "Wyszukaj typy autoryzacji...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Nie znaleziono typu autoryzacji",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Autoryzacja: klucz Bearer. Używane przez OpenAI i większość dostawców",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-klucz-api",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "nagłówek x-klucz-api. Używane przez Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-klucz-api",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "nagłówek x-goog-klucz-api. Używane przez Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Autoryzacja: klucz Splunk. Używane przez Splunk HTTP Event Collector",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-autoryzacja: klucz Bearer. Używane przez Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "Bez autoryzacji",
|
||||||
|
"aiProviderAuthTypePassthrough": "Przejście",
|
||||||
|
"aiProviderAuthTypeDescription": "Jak upstream API autoryzuje zapytania",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Przekaż nagłówki klucza API dzwoniącego do upstream",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Nie wysyłaj nagłówków autoryzacyjnych do upstream",
|
||||||
|
"aiProviderRoutingMode": "Tryb trasowania",
|
||||||
|
"aiProviderRoutingModeDescription": "Wysyłaj ruch do URL-u w górnym kierunku lub do celów HTTP na Twoich witrynach",
|
||||||
|
"aiProviderRoutingModeUrl": "URL do góry",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Zadzwoń na publiczny lub prywatny podstawowy URL API",
|
||||||
|
"aiProviderRoutingModeTarget": "Cele witryny",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Trasa przez cele na Twoich witrynach",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Po utworzeniu tego dostawcy, skonfiguruj cele witryny na karcie Ustawienia sieci.",
|
||||||
|
"aiProviderTargetNoOne": "Ten dostawca nie ma żadnych celów. Dodaj cel, aby trasować zapytania przez swoje witryny.",
|
||||||
|
"aiProviderSkipTlsVerification": "Pomiń weryfikację TLS",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Wyłącz weryfikację certyfikatu TLS dla połączenia w górę",
|
||||||
|
"aiProviderBudget": "Budżet",
|
||||||
|
"aiProviderBudgetDescription": "Opcjonalny budżet wydatków lub tokena dla tego dostawcy",
|
||||||
|
"aiProviderBudgetAmount": "Kwota budżetu",
|
||||||
|
"aiProviderBudgetUnit": "Jednostka budżetu",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Tokeny",
|
||||||
|
"aiProviderEnabled": "Włączone",
|
||||||
|
"aiProviderEnabledDescription": "Całkowicie wyłącz tego dostawcę we wszystkich zasobach",
|
||||||
|
"aiProviderErrorCreate": "Nie udało się utworzyć dostawcy AI",
|
||||||
|
"aiProviderErrorUpdate": "Nie udało się zaktualizować dostawcy AI",
|
||||||
|
"aiProviderErrorDelete": "Nie udało się usunąć dostawcy AI",
|
||||||
|
"aiProviderErrorLoad": "Nie udało się załadować dostawcy AI",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Wprowadź prawidłowy URL w górę",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "W tym dostawcy wymagany jest URL w górnym kierunku",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Wymagany typ autoryzacji",
|
||||||
|
"aiProviderErrorApiKeyRequired": "Wymagany klucz API",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Trasowanie celów witryny jest dostępne tylko dla niestandardowych dostawców",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Wybierz przynajmniej jedną zdolność API",
|
||||||
|
"aiProviderCapabilities": "Zdolności API",
|
||||||
|
"aiProviderCapabilitiesDescription": "Wybierz, które formaty API ten dostawca może obsługiwać. Znani dostawcy zaczynają od zalecanych domyślnych.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Wybierz, które formaty API ten niestandardowy dostawca może obsłużyć",
|
||||||
|
"aiProviderCapabilitiesSelect": "Wybierz możliwości",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Nie znaleziono możliwości",
|
||||||
|
"aiProviderCapabilitiesSearch": "Szukaj możliwości...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Chat Ukończone",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Obsługuje /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI Odpowiedzi",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Obsługuje /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic Wiadomości",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Obsługuje /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini Generowanie Treści",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Obsługuje bezpośredni Gemini API",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Model Bedrock Wywołanie",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Obsługuje Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex Generowanie Treści",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Obsługuje Vertex AI Gemini format",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex Surowe Predykcje",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Obsługuje Vertex AI przewidywania surowe dla modeli Anthropic",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock Rozmowa",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Obsługuje Amazon Bedrock Converse API",
|
||||||
|
"aiProviderCreated": "Dostawca AI utworzony",
|
||||||
|
"aiProviderUpdated": "Dostawca AI zaktualizowany",
|
||||||
|
"aiProviderDeleted": "Dostawca AI usunięty",
|
||||||
|
"aiProviderDelete": "Usuń Dostawcę",
|
||||||
|
"aiProviderDeleteConfirm": "Usuń Dostawcę",
|
||||||
|
"aiProviderQuestionRemove": "Czy na pewno chcesz usunąć tego dostawcę AI?",
|
||||||
|
"aiProviderMessageRemove": "To na zawsze usunie dostawcę i jego modele oraz cele. Tego nie można cofnąć.",
|
||||||
|
"aiProviderErrorNoUpdate": "Dostawca AI nie jest dostępny do aktualizacji",
|
||||||
|
"aiProviderModels": "Modele",
|
||||||
|
"aiProviderModelsDescription": "Zdefiniuj listy dozwolone i zablokowane dla tego dostawcy. Żądania muszą pasować do wpisu z listy dozwolonych i nie pasować do zablokowanych.",
|
||||||
|
"aiProviderCreateModelsDescription": "Wybierz, które modele ten dostawca może obsłużyć. Pusta lista dozwolonych odrzuca cały ruch. Możesz później dodać lub zmienić modele.",
|
||||||
|
"aiProviderModelsPlaceholder": "Szukaj modeli lub wpisz własny klucz",
|
||||||
|
"aiProviderModelsAllow": "Lista Dozwolonych",
|
||||||
|
"aiProviderModelsAllowDescription": "Modele, które można używać przez tego dostawcę. Pusta oznacza odrzucenie wszystkiego.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Wprowadź klucz modelu",
|
||||||
|
"aiProviderModelsAllowEmpty": "Nie dodano jeszcze żadnych modeli.",
|
||||||
|
"aiProviderModelsBlock": "Lista Zablokowanych",
|
||||||
|
"aiProviderModelsBlockDescription": "Modele do odrzucenia, nawet jeśli pasują do wpisu z listy dozwolonych.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Wprowadź klucz modelu",
|
||||||
|
"aiProviderModelsBlockEmpty": "Nie dodano jeszcze żadnych modeli.",
|
||||||
|
"aiProviderModelsAdd": "Dodaj Modele",
|
||||||
|
"aiProviderModelsClearAll": "Wyczyść Wszystko",
|
||||||
|
"aiProviderModelsAddCustom": "Dodaj \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Naciśnij Enter, aby dodać ten własny klucz modelu.",
|
||||||
|
"aiProviderModelsAddBulk": "Dodaj {count} własne klucze",
|
||||||
|
"aiProviderModelsAddBulkHint": "Naciśnij Enter, aby dodać {count} własne klucze.",
|
||||||
|
"aiProviderModelsAddOne": "Dodaj {key} teraz",
|
||||||
|
"aiProviderModelsAddSelected": "Dodaj Wybrane",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} wybrane",
|
||||||
|
"aiProviderModelsSelectAll": "Zaznacz wszystko",
|
||||||
|
"aiProviderModelsClearSelected": "Wyczyść",
|
||||||
|
"aiProviderModelsBulkHint": "Wybierz znane modele lub wpisz własny klucz.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Brak pasujących modeli w katalogu.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Znane Modele",
|
||||||
|
"aiProviderModelsAllLabel": "Wszystkie modele",
|
||||||
|
"aiProviderModelsAllPatternHint": "Dzika karta: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Zezwól na wszystkie modele",
|
||||||
|
"aiProviderModelsAddAllBlock": "Zablokuj wszystkie modele",
|
||||||
|
"aiProviderModelsAddAllDescription": "Używa dzikiej karty *, więc pasuje do każdego klucza modelu.",
|
||||||
|
"aiProviderModelsViewMore": "Zobacz więcej ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Zobacz mniej",
|
||||||
|
"aiProviderModelsRemove": "Usuń model",
|
||||||
|
"aiProviderModelsEditHint": "Kliknij, aby edytować ustawienia modelu",
|
||||||
|
"aiProviderModelsSourceCatalog": "Znany model katalogu",
|
||||||
|
"aiProviderModelsSourceCustom": "Własny klucz modelu",
|
||||||
|
"aiProviderModelsSourcePattern": "Wzór dzikiej karty",
|
||||||
|
"aiProviderModelsSourceAll": "Pasuje do każdego klucza modelu",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Budżet skonfigurowany",
|
||||||
|
"aiProviderModelsEditTitle": "Edytuj Model",
|
||||||
|
"aiProviderModelsEditDescription": "Zaktualizuj klucz modelu lub skonfiguruj jego budżet.",
|
||||||
|
"aiProviderModelsBudgetTab": "Budżet",
|
||||||
|
"aiProviderModelsKeyLabel": "Klucz Modelu",
|
||||||
|
"aiProviderModelsKeyRequired": "Wpisz klucz modelu",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Ten klucz modelu już jest na liście",
|
||||||
|
"aiProviderModelsOverlapError": "Te wzorce nie mogą być na obydwu listach: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modele zaktualizowane",
|
||||||
|
"aiProviderModelsErrorUpdate": "Nie udało się zaktualizować modeli",
|
||||||
|
"aiResourceProviders": "Dostawcy",
|
||||||
|
"aiResourceProvidersDescription": "Wybierz, z których dostawców AI ta brama AI może korzystać",
|
||||||
|
"aiResourceProvidersHelp": "Dołącz dostawców i wybierz dziedziczenie (użyj list każdego dostawcy) lub wybierz (wybierz listę dozwoloną dla tego zasobu). Wzory dozwolone, które konfliktują między dołączonymi dostawcami, nie są dozwolone.",
|
||||||
|
"aiResourceProvidersSelect": "Wybierz dostawców",
|
||||||
|
"aiResourceProvidersEmpty": "Nie znaleziono dostawców AI",
|
||||||
|
"aiResourceProvidersNoneAttached": "Nie dołączono jeszcze żadnych dostawców.",
|
||||||
|
"aiResourceProvidersAdd": "Dodaj dostawcę",
|
||||||
|
"aiResourceProvidersRemove": "Usuń dostawcę",
|
||||||
|
"aiResourceProviderToggleEnabled": "Włącz lub wyłącz tego dostawcę na zasobie",
|
||||||
|
"aiResourceProviderDisabled": "Wyłączone",
|
||||||
|
"aiResourceProvidersUpdated": "Dostawcy zaktualizowani",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Nie udało się zaktualizować dostawców",
|
||||||
|
"aiResourceProviderEditDescription": "Wybierz, jak modele tego dostawcy są udostępniane na tym zasobie.",
|
||||||
|
"viewProviderSettings": "Zobacz Ustawienia Dostawcy",
|
||||||
|
"aiResourceProviderMode": "Tryb dostępu",
|
||||||
|
"aiResourceProviderModeInherit": "Dziedziczenie",
|
||||||
|
"aiResourceProviderModeSelect": "Wybierz",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Wybierz · {count} modele",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Użyj list dozwolonych i zablokowanych tego dostawcy, skonfigurowanych na dostawcy.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Wybierz podzbiór modeli z listy dozwolonych tego dostawcy dla tego zasobu.",
|
||||||
|
"aiResourceProviderAllowModels": "Lista dozwolonych",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Wybierz modele",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Szukaj modeli...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Nie znaleziono modeli",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Tylko modele z listy dozwolonych tego dostawcy mogą być wybrane.",
|
||||||
|
"aiResourceAliasRequired": "Alias jest wymagany dla bramy AI",
|
||||||
|
"aiResourceDomainConfiguration": "Konfiguracja Domeny",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Wybierz domenę, której klienci będą używać, aby dotrzeć do tej bramy AI.",
|
||||||
|
"aiUsageAnalyticsTitle": "AI Użycie Analityki",
|
||||||
|
"aiUsageAnalyticsDescription": "Analizuj koszty bramy AI, użycie tokenów i aktywność wśród dostawców, zasobów, ról i użytkowników",
|
||||||
|
"aiUsageTabOverview": "Przegląd",
|
||||||
|
"aiUsageTabProviders": "Dostawcy",
|
||||||
|
"aiUsageTabResources": "Zasoby",
|
||||||
|
"aiUsageFilterProvider": "Dostawca",
|
||||||
|
"aiUsageFilterModel": "Model",
|
||||||
|
"aiUsageFilterResource": "Zasób",
|
||||||
|
"aiUsageFilterRole": "Rola",
|
||||||
|
"aiUsageFilterUser": "Użytkownik",
|
||||||
|
"aiUsageFilterAllProviders": "Wszyscy Dostawcy",
|
||||||
|
"aiUsageFilterAllModels": "Wszystkie Modele",
|
||||||
|
"aiUsageFilterAllResources": "Wszystkie Zasoby",
|
||||||
|
"aiUsageFilterAllRoles": "Wszystkie Role",
|
||||||
|
"aiUsageFilterAllUsers": "Wszyscy Użytkownicy",
|
||||||
|
"aiUsageFilterSearch": "Szukaj...",
|
||||||
|
"aiUsageFilterNotFound": "Nie znaleziono opcji",
|
||||||
|
"aiUsageResetFilters": "Resetuj Filtry",
|
||||||
|
"aiUsageRefresh": "Odśwież",
|
||||||
|
"aiUsageTokenTypePrompt": "Podpowiedź",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Odczyt z bufora",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Zapis do bufora",
|
||||||
|
"aiUsageTokenTypeCompletion": "Ukończenie",
|
||||||
|
"aiUsageTokenTypeReasoning": "Rozumowanie",
|
||||||
|
"aiUsageRequests": "Żądania",
|
||||||
|
"aiUsageCost": "Koszt",
|
||||||
|
"aiUsageTokens": "Tokeny",
|
||||||
|
"aiUsageOther": "Inne",
|
||||||
|
"aiUsageTotalRequests": "Łączna liczba Żądań",
|
||||||
|
"aiUsageTotalTokens": "Łączna liczba Tokenów",
|
||||||
|
"aiUsageTotalCost": "Całkowity Koszt",
|
||||||
|
"aiUsageEstimated": "Szacowany",
|
||||||
|
"aiUsageRequestVolume": "Objętość żądań",
|
||||||
|
"aiUsageTokenUsage": "Użycie Tokenów",
|
||||||
|
"aiUsageModelCost": "Koszt Modelu",
|
||||||
|
"aiUsageModelTokens": "Tokeny Modelu",
|
||||||
|
"aiUsageTopModels": "Najlepsze Modele",
|
||||||
|
"aiUsageTopProviders": "Najlepsi Dostawcy",
|
||||||
|
"aiUsageProviderCost": "Koszt Dostawcy",
|
||||||
|
"aiUsageProviderTokenUsage": "Użycie Tokenów Dostawcy",
|
||||||
|
"aiUsageTopResources": "Najlepsze Zasoby",
|
||||||
|
"aiUsageResourceCost": "Koszt Zasobu",
|
||||||
|
"aiUsageResourceTokenUsage": "Użycie Tokenów Zasobu",
|
||||||
|
"aiUsageResourceTypePublic": "Publiczny Zasób",
|
||||||
|
"aiUsageResourceTypeSite": "Prywatny Zasób",
|
||||||
|
"aiUsageNoResource": "Brak zasobu",
|
||||||
|
"aiUsageRolesTab": "Role",
|
||||||
|
"aiUsageUsersTab": "Użytkownicy",
|
||||||
|
"aiUsageTopRoles": "Najlepsze Role",
|
||||||
|
"aiUsageRoleCost": "Koszt Roli",
|
||||||
|
"aiUsageRoleTokenUsage": "Użycie Tokenów Roli",
|
||||||
|
"aiUsageTopUsers": "Najwięcej Użytkowników",
|
||||||
|
"aiUsageUserCost": "Koszt Użytkownika",
|
||||||
|
"aiUsageUserTokenUsage": "Użycie Tokenów Użytkownika",
|
||||||
|
"aiUsageUnknownUser": "Nieznany użytkownik",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Wirtualne Klucze API",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Wirtualny Klucz API",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Wszystkie Wirtualne Klucze API",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Najlepsze Wirtualne Klucze API",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Koszt Wirtualnego Klucza API",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Użycie Tokenów Wirtualnego Klucza API",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Brak wirtualnego klucza API",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Bezimienny klucz",
|
||||||
|
"aiUsageLoading": "Ładowanie...",
|
||||||
|
"aiUsageNoData": "Brak danych",
|
||||||
|
"resourceBudgetSettings": "Budżet",
|
||||||
|
"resourceBudgetSettingsDescription": "Skonfiguruj, jak ta brama AI ogranicza użycie na podstawie limitów wydatków lub tokenów",
|
||||||
"sidebarApiKeys": "Klucze API",
|
"sidebarApiKeys": "Klucze API",
|
||||||
"sidebarProvisioning": "Dostarczanie",
|
"sidebarProvisioning": "Dostarczanie",
|
||||||
"sidebarSettings": "Ustawienia",
|
"sidebarSettings": "Ustawienia",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Podsieć",
|
"subnetPlaceholder": "Podsieć",
|
||||||
"addressDescription": "Adres wewnętrzny klienta. Musi mieścić się w podsieci organizacji.",
|
"addressDescription": "Adres wewnętrzny klienta. Musi mieścić się w podsieci organizacji.",
|
||||||
"selectSites": "Wybierz witryny",
|
"selectSites": "Wybierz witryny",
|
||||||
|
"selectResources": "Wybierz zasoby",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# zasób} few {# zasoby} many {# zasobów} other {# zasobów}}",
|
||||||
"selectLabels": "Wybierz etykiety",
|
"selectLabels": "Wybierz etykiety",
|
||||||
"sitesDescription": "Klient będzie miał łączność z wybranymi witrynami",
|
"sitesDescription": "Klient będzie miał łączność z wybranymi witrynami",
|
||||||
"clientInstallOlm": "Zainstaluj Olm",
|
"clientInstallOlm": "Zainstaluj Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Czas w sekundach",
|
"timeIsInSeconds": "Czas w sekundach",
|
||||||
"requireDeviceApproval": "Wymagaj zatwierdzenia urządzenia",
|
"requireDeviceApproval": "Wymagaj zatwierdzenia urządzenia",
|
||||||
"requireDeviceApprovalDescription": "Użytkownicy o tej roli potrzebują nowych urządzeń zatwierdzonych przez administratora, zanim będą mogli połączyć się i uzyskać dostęp do zasobów.",
|
"requireDeviceApprovalDescription": "Użytkownicy o tej roli potrzebują nowych urządzeń zatwierdzonych przez administratora, zanim będą mogli połączyć się i uzyskać dostęp do zasobów.",
|
||||||
"sshSettings": "Ustawienia SSH",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "Brama AI",
|
||||||
"sshAccess": "Dostęp SSH",
|
"sshAccess": "Dostęp SSH",
|
||||||
"rdpSettings": "Ustawienia RDP",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "Ustawienia VNC",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "Serwer SSH",
|
"sshServer": "Serwer SSH",
|
||||||
"rdpServer": "Serwer RDP",
|
"rdpServer": "Serwer RDP",
|
||||||
"vncServer": "Serwer VNC",
|
"vncServer": "Serwer VNC",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "Brama AI",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Schemat",
|
"editInternalResourceDialogScheme": "Schemat",
|
||||||
"editInternalResourceDialogEnableSsl": "Włącz TLS",
|
"editInternalResourceDialogEnableSsl": "Włącz TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Zezwalaj na ping ICMP",
|
"privateResourceAllowIcmpPing": "Zezwalaj na ping ICMP",
|
||||||
"privateResourceNetworkAccess": "Dostęp do sieci",
|
"privateResourceNetworkAccess": "Dostęp do sieci",
|
||||||
"privateResourceNetworkAccessDescription": "Kontroluj dostęp do portów TCP/UDP i czy ping ICMP jest dozwolony dla tego zasobu.",
|
"privateResourceNetworkAccessDescription": "Kontroluj dostęp do portów TCP/UDP i czy ping ICMP jest dozwolony dla tego zasobu.",
|
||||||
"hostSettings": "Ustawienia hosta",
|
"hostSettings": "Host",
|
||||||
"cidrSettings": "Ustawienia CIDR",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Właściwości zasobów",
|
"createInternalResourceDialogResourceProperties": "Właściwości zasobów",
|
||||||
"createInternalResourceDialogName": "Nazwa",
|
"createInternalResourceDialogName": "Nazwa",
|
||||||
"createInternalResourceDialogSite": "Witryna",
|
"createInternalResourceDialogSite": "Witryna",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "Brama AI",
|
||||||
"scheme": "Schemat",
|
"scheme": "Schemat",
|
||||||
"createInternalResourceDialogScheme": "Schemat",
|
"createInternalResourceDialogScheme": "Schemat",
|
||||||
"createInternalResourceDialogEnableSsl": "Włącz TLS",
|
"createInternalResourceDialogEnableSsl": "Włącz TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Prawidłowe hasło",
|
"validPassword": "Prawidłowe hasło",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Ważny Wirtualny Klucz API",
|
||||||
"view": "Zobacz",
|
"view": "Zobacz",
|
||||||
"configManaged": "Konfiguracja zarządzana",
|
"configManaged": "Konfiguracja zarządzana",
|
||||||
"connectedClient": "Połączony Klient",
|
"connectedClient": "Połączony Klient",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Zachowanie dziennika",
|
"logRetention": "Zachowanie dziennika",
|
||||||
"logRetentionDescription": "Zarządzaj jak długo różne typy logów są zachowane dla tej organizacji lub wyłącz je",
|
"logRetentionDescription": "Zarządzaj jak długo różne typy logów są zachowane dla tej organizacji lub wyłącz je",
|
||||||
"requestLogsDescription": "Zobacz szczegółowe dzienniki żądań zasobów w tej organizacji",
|
"requestLogsDescription": "Zobacz szczegółowe dzienniki żądań zasobów w tej organizacji",
|
||||||
|
"aiSessionLogs": "Dzienniki Sesji Bramy AI",
|
||||||
|
"aiSessionLogsDescription": "Zobacz transkrypcje podpowiedzi i odpowiedzi dla żądań bramy AI w tej organizacji",
|
||||||
|
"sidebarLogsAi": "Dzienniki Sesji",
|
||||||
|
"commandLogsAi": "Dzienniki Sesji",
|
||||||
|
"sidebarLogsAiUsage": "Analiza Użycia",
|
||||||
|
"commandLogsAiUsage": "Analiza Użycia",
|
||||||
|
"provider": "Dostawca",
|
||||||
|
"capability": "Zdolność",
|
||||||
|
"model": "Model",
|
||||||
|
"virtualApiKey": "Wirtualny Klucz API",
|
||||||
|
"noVirtualApiKey": "Brak wirtualnego klucza API",
|
||||||
|
"stream": "Strumień",
|
||||||
|
"streaming": "Strumieniowanie",
|
||||||
|
"nonStreaming": "Niestrumieniowane",
|
||||||
|
"statusCode": "Kod Stanu",
|
||||||
|
"aiSessionId": "Identyfikator Sesji",
|
||||||
|
"aiSessionRequest": "Żądanie",
|
||||||
|
"aiSessionResponse": "Odpowiedź",
|
||||||
|
"aiSessionNoData": "Brak danych do przechwycenia",
|
||||||
|
"aiSessionCouldNotParse": "(nieprzetworzone, nie można było parsować transkrypcji)",
|
||||||
|
"aiSessionLogTruncated": "Ta sesja została przycięta przed przechowywaniem i może być niekompletna.",
|
||||||
|
"aiSessionViewRaw": "Zobacz Raw JSON",
|
||||||
|
"aiSessionViewChat": "Zobacz Czat",
|
||||||
|
"cost": "Koszt",
|
||||||
|
"estimated": "Szacowany",
|
||||||
|
"tokenUsage": "Użycie Tokenów",
|
||||||
|
"promptTokens": "Tokeny Podpowiedzi",
|
||||||
|
"cacheReadTokens": "Tokeny Odczytu Bufora",
|
||||||
|
"cacheWriteTokens": "Tokeny Zapisu Bufora",
|
||||||
|
"completionTokens": "Tokeny Ukończenia",
|
||||||
|
"reasoningTokens": "Tokeny Rozumowania",
|
||||||
|
"totalTokens": "Całkowita Liczba Tokenów",
|
||||||
"requestAnalyticsDescription": "Zobacz szczegółowe analizy żądań dla zasobów w tej organizacji",
|
"requestAnalyticsDescription": "Zobacz szczegółowe analizy żądań dla zasobów w tej organizacji",
|
||||||
"logRetentionRequestLabel": "Przechowywanie dzienników żądań HTTP",
|
"logRetentionRequestLabel": "Przechowywanie dzienników żądań HTTP",
|
||||||
"logRetentionRequestDescription": "Jak długo zachować dzienniki żądań",
|
"logRetentionRequestDescription": "Jak długo zachować dzienniki żądań",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Jak długo zachować dzienniki akcji",
|
"logRetentionActionDescription": "Jak długo zachować dzienniki akcji",
|
||||||
"logRetentionConnectionLabel": "Zachowanie dziennika połączeń",
|
"logRetentionConnectionLabel": "Zachowanie dziennika połączeń",
|
||||||
"logRetentionConnectionDescription": "Jak długo zachować dzienniki połączeń",
|
"logRetentionConnectionDescription": "Jak długo zachować dzienniki połączeń",
|
||||||
|
"logRetentionAISessionsLabel": "Retencja Dzienników Sesji Bramy AI",
|
||||||
|
"logRetentionAISessionsDescription": "Jak długo przechowywać dzienniki sesji podpowiedzi/odpowiedzi bramy AI",
|
||||||
"logRetentionDisabled": "Wyłączone",
|
"logRetentionDisabled": "Wyłączone",
|
||||||
"logRetention3Days": "3 dni",
|
"logRetention3Days": "3 dni",
|
||||||
"logRetention7Days": "7 dni",
|
"logRetention7Days": "7 dni",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Adres źródłowy",
|
"sourceAddress": "Adres źródłowy",
|
||||||
"destinationAddress": "Adres docelowy",
|
"destinationAddress": "Adres docelowy",
|
||||||
"duration": "Czas trwania",
|
"duration": "Czas trwania",
|
||||||
"licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lub <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Zarezerwuj darmową wersję demo lub próbę POC, aby dowiedzieć się więcej.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Licencja Wydania Enterprise</enterpriseEditionLink> jest wymagana. <bookADemoLink>Zarezerwuj demo lub próbę</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> jest wymagany do korzystania z tej funkcji. Ta funkcja jest również dostępna w <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Zarezerwuj darmową wersję demo lub próbę POC, aby dowiedzieć się więcej.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Licencja Wydania Enterprise</enterpriseEditionLink> jest wymagana. <bookADemoLink>Zarezerwuj demo lub próbę</bookADemoLink>",
|
||||||
"certResolver": "Rozwiązywanie certyfikatów",
|
"certResolver": "Rozwiązywanie certyfikatów",
|
||||||
"certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.",
|
"certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.",
|
||||||
"selectCertResolver": "Wybierz Resolver certyfikatów",
|
"selectCertResolver": "Wybierz Resolver certyfikatów",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Wybór wielu stron umożliwia odporne trasowanie i awarię dla wysokiej dostępności.",
|
"internalResourceFormMultiSiteRoutingHelp": "Wybór wielu stron umożliwia odporne trasowanie i awarię dla wysokiej dostępności.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Dowiedz się więcej",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Dowiedz się więcej",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Ogranicz dostęp do konkretnych portów TCP/UDP lub zezwól/zablokuj wszystkie porty.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Ogranicz dostęp do konkretnych portów TCP/UDP lub zezwól/zablokuj wszystkie porty.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "Konfiguracja HTTP",
|
"createInternalResourceDialogHttpConfiguration": "Konfiguracja Domeny",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Wybierz domenę, której klienci będą używać, aby dotrzeć do tego zasobu przez HTTP lub HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Wybierz domenę, której klienci będą używać, aby dotrzeć do tego zasobu przez HTTP lub HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "Konfiguracja HTTP",
|
"editInternalResourceDialogHttpConfiguration": "Konfiguracja Domeny",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Wybierz domenę, której klienci będą używać, aby dotrzeć do tego zasobu przez HTTP lub HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Wybierz domenę, której klienci będą używać, aby dotrzeć do tego zasobu przez HTTP lub HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Bez etykiety",
|
"resourceLauncherUnlabeled": "Bez etykiety",
|
||||||
|
"resourceLauncherAiGateway": "Brama AI",
|
||||||
"resourceLauncherNoSite": "Brak strony",
|
"resourceLauncherNoSite": "Brak strony",
|
||||||
|
"resourceLauncherAvailableModels": "Dostępne Modele",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modele, z których możesz korzystać z tą bramą AI.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Brak dostępnych modeli dla tego zasobu.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Nie można załadować dostępnych modeli.",
|
||||||
|
"resourceLauncherApiKeys": "Klucze API",
|
||||||
|
"resourceLauncherApiKeysDescription": "Użyj klucza tożsamości lub przypisanego klucza, aby uwierzytelnić się przy użyciu tego zasobu.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Klucz Tożsamości",
|
||||||
|
"resourceLauncherApiKeysManual": "Klucze Przypisane",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Brak dostępnych kluczy API dla tego zasobu.",
|
||||||
|
"resourceLauncherApiKeysError": "Nie można załadować kluczy API.",
|
||||||
"resourceLauncherNoResourcesInGroup": "W tej grupie nie ma zasobów",
|
"resourceLauncherNoResourcesInGroup": "W tej grupie nie ma zasobów",
|
||||||
"resourceLauncherEmptyStateTitle": "Brak dostępnych zasobów",
|
"resourceLauncherEmptyStateTitle": "Brak dostępnych zasobów",
|
||||||
"resourceLauncherEmptyStateDescription": "Jeszcze nie masz dostępu do żadnych zasobów. Skontaktuj się z administratorem, aby poprosić o dostęp.",
|
"resourceLauncherEmptyStateDescription": "Jeszcze nie masz dostępu do żadnych zasobów. Skontaktuj się z administratorem, aby poprosić o dostęp.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Nie można usunąć widoku uruchamiacza. Proszę spróbować ponownie.",
|
"resourceLauncherViewDeleteFailedDescription": "Nie można usunąć widoku uruchamiacza. Proszę spróbować ponownie.",
|
||||||
"memberPortalPrevious": "Poprzedni",
|
"memberPortalPrevious": "Poprzedni",
|
||||||
"memberPortalNext": "Następny",
|
"memberPortalNext": "Następny",
|
||||||
"httpSettings": "Ustawienia HTTP",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "Ustawienia TCP",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "Ustawienia UDP",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Nawiązywanie bezpiecznego połączenia…",
|
"sshConnectingDescription": "Nawiązywanie bezpiecznego połączenia…",
|
||||||
"sshConnecting": "Łączenie…",
|
"sshConnecting": "Łączenie…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Verifique se você está logado como o utilizador correto.",
|
"inviteLoginUser": "Verifique se você está logado como o utilizador correto.",
|
||||||
"inviteErrorNoUser": "Desculpe, mas parece que o convite que está a tentar aceder não é para um utilizador que existe.",
|
"inviteErrorNoUser": "Desculpe, mas parece que o convite que está a tentar aceder não é para um utilizador que existe.",
|
||||||
"inviteCreateUser": "Por favor, crie uma conta primeiro.",
|
"inviteCreateUser": "Por favor, crie uma conta primeiro.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Convites só podem ser aceitos por contas internas. Faça logout e acesse com sua senha para este e-mail.",
|
||||||
|
"inviteLoginInternalOnly": "Convites exigem uma conta interna com uma senha. Crie uma conta ou faça login com sua senha.",
|
||||||
"goHome": "Voltar ao inicio",
|
"goHome": "Voltar ao inicio",
|
||||||
"inviteLogInOtherUser": "Fazer login como um utilizador diferente",
|
"inviteLogInOtherUser": "Fazer login como um utilizador diferente",
|
||||||
"createAnAccount": "Crie uma conta",
|
"createAnAccount": "Crie uma conta",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Nenhum país encontrado.",
|
"noCountryFound": "Nenhum país encontrado.",
|
||||||
"siteSelectionDescription": "Este site fornecerá conectividade ao destino.",
|
"siteSelectionDescription": "Este site fornecerá conectividade ao destino.",
|
||||||
"resourceType": "Tipo de Recurso",
|
"resourceType": "Tipo de Recurso",
|
||||||
"resourceTypeDescription": "Isso controla o protocolo do recurso e como ele será renderizado no navegador. Isso não pode ser alterado posteriormente.",
|
"resourceTypeDescription": "Isso controla o protocolo do recurso e não pode ser alterado posteriormente.",
|
||||||
|
"resourceTypeSearch": "Procurar tipos de recursos...",
|
||||||
|
"resourceTypeNotFound": "Nenhum tipo de recurso encontrado",
|
||||||
|
"resourceTypeHttpDescription": "Proxy de tráfego web sobre HTTPS usando um nome de domínio",
|
||||||
|
"resourceTypeInferenceDescription": "Roteie solicitações de IA através de provedores conectados",
|
||||||
|
"resourceTypeSshDescription": "Acesse um servidor SSH a partir do navegador",
|
||||||
|
"resourceTypeRdpDescription": "Acesse uma área de trabalho remota a partir do navegador",
|
||||||
|
"resourceTypeVncDescription": "Acesse uma área de trabalho VNC a partir do navegador",
|
||||||
|
"resourceTypeTcpDescription": "Proxy de tráfego TCP bruto usando um número de porta",
|
||||||
|
"resourceTypeUdpDescription": "Proxy de tráfego UDP bruto usando um número de porta",
|
||||||
|
"privateResourceTypeDescription": "Isso controla como os clientes alcançam o recurso. Não pode ser alterado posteriormente.",
|
||||||
|
"privateResourceTypeHostDescription": "Exponha um único host na rede do site para clientes conectados",
|
||||||
|
"privateResourceTypeCidrDescription": "Exponha um intervalo CIDR na rede do site para clientes conectados",
|
||||||
|
"privateResourceTypeHttpDescription": "Acesse um serviço HTTP ou HTTPS por meio de um domínio",
|
||||||
|
"privateResourceTypeSshDescription": "Acesse um servidor SSH de clientes conectados",
|
||||||
"resourceDomainDescription": "O recurso será servido neste nome de domínio totalmente qualificado.",
|
"resourceDomainDescription": "O recurso será servido neste nome de domínio totalmente qualificado.",
|
||||||
"resourceHTTPSSettings": "Configurações de HTTPS",
|
"resourceHTTPSSettings": "Configurações de HTTPS",
|
||||||
"resourceHTTPSSettingsDescription": "Configure como o recurso será acessado por HTTPS",
|
"resourceHTTPSSettingsDescription": "Configure como o recurso será acessado por HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Salvar Configurações",
|
"authMethodsSave": "Salvar Configurações",
|
||||||
"policyAuthStackTitle": "Autenticação",
|
"policyAuthStackTitle": "Autenticação",
|
||||||
"policyAuthStackDescription": "Controle quais métodos de autenticação são necessários para acessar este recurso",
|
"policyAuthStackDescription": "Controle quais métodos de autenticação são necessários para acessar este recurso",
|
||||||
|
"policyAuthInferenceStackDescription": "Escolha quais usuários e funções podem autenticar com este gateway de IA",
|
||||||
"policyAuthOrLogicTitle": "Vários métodos de autenticação ativos",
|
"policyAuthOrLogicTitle": "Vários métodos de autenticação ativos",
|
||||||
"policyAuthOrLogicBanner": "Os visitantes podem autenticar-se usando qualquer um dos métodos ativos abaixo. Eles não precisam completar todos eles.",
|
"policyAuthOrLogicBanner": "Os visitantes podem autenticar-se usando qualquer um dos métodos ativos abaixo. Eles não precisam completar todos eles.",
|
||||||
"policyAuthMethodActive": "Ativo",
|
"policyAuthMethodActive": "Ativo",
|
||||||
"policyAuthMethodOff": "Desligado",
|
"policyAuthMethodOff": "Desligado",
|
||||||
"policyAuthSsoTitle": "SSO da Plataforma",
|
"policyAuthSsoTitle": "SSO da Plataforma",
|
||||||
"policyAuthSsoDescription": "Exigir login pelo provedor de identidade da sua organização",
|
"policyAuthSsoDescription": "Exigir login pelo provedor de identidade da sua organização",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "URL de entrada",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Todo usuário já possui uma chave de API de identidade, portanto, você só precisa criar chaves de API virtuais para clientes que não são de usuários ou acesso compartilhado. Os usuários podem recuperar sua chave fazendo login com seu provedor de identidade no URL deste recurso, onde ela será mostrada após o login.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Todo usuário já possui uma chave de API de identidade, então você só precisa criar chaves de API virtuais para clientes que não são de usuários ou acesso compartilhado. Os usuários recuperam sua chave aqui após fazer login com seu provedor de identidade.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} usuários, {roles} funções",
|
"policyAuthSsoSummary": "{idp} · {users} usuários, {roles} funções",
|
||||||
"policyAuthSsoDefaultIdp": "Provedor padrão",
|
"policyAuthSsoDefaultIdp": "Provedor padrão",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Adicionar Provedor de Identidade Padrão",
|
"policyAuthAddDefaultIdentityProvider": "Adicionar Provedor de Identidade Padrão",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Nova função é necessária",
|
"accessRoleErrorNewRequired": "Nova função é necessária",
|
||||||
"accessRoleErrorRemove": "Falha ao remover função",
|
"accessRoleErrorRemove": "Falha ao remover função",
|
||||||
"accessRoleErrorRemoveDescription": "Ocorreu um erro ao remover a função.",
|
"accessRoleErrorRemoveDescription": "Ocorreu um erro ao remover a função.",
|
||||||
|
"accessRoleInferenceBudget": "Orçamento de IA",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Configure como membros desta função restringem o uso de IA com base nos limites de gasto ou token",
|
||||||
"accessRoleName": "Nome da Função",
|
"accessRoleName": "Nome da Função",
|
||||||
"accessRoleQuestionRemove": "Você está prestes a apagar o papel `{name}. Você não pode desfazer esta ação.",
|
"accessRoleQuestionRemove": "Você está prestes a apagar o papel `{name}. Você não pode desfazer esta ação.",
|
||||||
"accessRoleRemove": "Remover Função",
|
"accessRoleRemove": "Remover Função",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Eliminar Site",
|
"actionDeleteSite": "Eliminar Site",
|
||||||
"actionGetSite": "Obter Site",
|
"actionGetSite": "Obter Site",
|
||||||
"actionListSites": "Listar Sites",
|
"actionListSites": "Listar Sites",
|
||||||
|
"actionCreateAiProvider": "Criar Provedor de IA",
|
||||||
|
"actionDeleteAiProvider": "Excluir Provedor de IA",
|
||||||
|
"actionGetAiProvider": "Obter Provedor de IA",
|
||||||
|
"actionListAiProviders": "Listar Provedores de IA",
|
||||||
|
"actionUpdateAiProvider": "Atualizar Provedor de IA",
|
||||||
|
"actionCreateAiModel": "Criar Modelo de IA",
|
||||||
|
"actionDeleteAiModel": "Excluir Modelo de IA",
|
||||||
|
"actionGetAiModel": "Obter Modelo de IA",
|
||||||
|
"actionListAiModels": "Listar Modelos de IA",
|
||||||
|
"actionUpdateAiModel": "Atualizar Modelo de IA",
|
||||||
|
"actionCreateAiBudget": "Criar Orçamento de IA",
|
||||||
|
"actionDeleteAiBudget": "Excluir Orçamento de IA",
|
||||||
|
"actionGetAiBudget": "Obter Orçamento de IA",
|
||||||
|
"actionListAiBudgets": "Listar Orçamentos de IA",
|
||||||
|
"actionUpdateAiBudget": "Atualizar Orçamento de IA",
|
||||||
|
"actionListResourceAiModels": "Listar Modelos de IA de Recursos",
|
||||||
|
"actionSetResourceAiModels": "Definir Modelos de IA de Recurso",
|
||||||
|
"actionCreateVirtualApiKey": "Criar Chave de API Virtual",
|
||||||
|
"actionDeleteVirtualApiKey": "Excluir Chave de API Virtual",
|
||||||
|
"actionGetVirtualApiKey": "Obter Chave de API Virtual",
|
||||||
|
"actionListVirtualApiKeys": "Listar Chaves de API Virtuais",
|
||||||
|
"actionUpdateVirtualApiKey": "Atualizar Chave de API Virtual",
|
||||||
"actionApplyBlueprint": "Aplicar Diagrama",
|
"actionApplyBlueprint": "Aplicar Diagrama",
|
||||||
"actionListBlueprints": "Listar Modelos",
|
"actionListBlueprints": "Listar Modelos",
|
||||||
"actionGetBlueprint": "Obter Modelo",
|
"actionGetBlueprint": "Obter Modelo",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Definir Código PIN do Recurso",
|
"actionSetResourcePincode": "Definir Código PIN do Recurso",
|
||||||
"actionSetResourceEmailWhitelist": "Definir Lista Permitida de Emails do Recurso",
|
"actionSetResourceEmailWhitelist": "Definir Lista Permitida de Emails do Recurso",
|
||||||
"actionGetResourceEmailWhitelist": "Obter Lista Permitida de Emails do Recurso",
|
"actionGetResourceEmailWhitelist": "Obter Lista Permitida de Emails do Recurso",
|
||||||
|
"actionListResourcePolicies": "Listar Políticas de Recursos",
|
||||||
|
"actionCreateResourcePolicy": "Criar Política de Recurso",
|
||||||
"actionGetResourcePolicy": "Obter Política de Recurso",
|
"actionGetResourcePolicy": "Obter Política de Recurso",
|
||||||
"actionUpdateResourcePolicy": "Atualizar Política de Recurso",
|
"actionUpdateResourcePolicy": "Atualizar Política de Recurso",
|
||||||
|
"actionDeleteResourcePolicy": "Excluir Política de Recurso",
|
||||||
"actionSetResourcePolicyUsers": "Definir Utilizadores da Política de Recurso",
|
"actionSetResourcePolicyUsers": "Definir Utilizadores da Política de Recurso",
|
||||||
"actionSetResourcePolicyRoles": "Definir Papéis da Política de Recurso",
|
"actionSetResourcePolicyRoles": "Definir Papéis da Política de Recurso",
|
||||||
"actionSetResourcePolicyPassword": "Definir Palavra-passe da Política de Recurso",
|
"actionSetResourcePolicyPassword": "Definir Palavra-passe da Política de Recurso",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Convites",
|
"sidebarInvitations": "Convites",
|
||||||
"sidebarRoles": "Papéis",
|
"sidebarRoles": "Papéis",
|
||||||
"sidebarShareableLinks": "Links Compartilháveis",
|
"sidebarShareableLinks": "Links Compartilháveis",
|
||||||
|
"sidebarAiGateway": "Gateway de IA",
|
||||||
|
"sidebarAiProviders": "Fornecedores",
|
||||||
|
"commandAiProviders": "Provedores de IA",
|
||||||
|
"sidebarVirtualApiKeys": "Chaves de API Virtuais",
|
||||||
|
"sidebarMyApiKeys": "Suas Chaves de API",
|
||||||
|
"sidebarAccount": "Lançador",
|
||||||
|
"commandVirtualApiKeys": "Chaves de API Virtuais",
|
||||||
|
"virtualApiKeysTitle": "Gerir Chaves de API Virtuais",
|
||||||
|
"virtualApiKeysDescription": "Crie e gerencie chaves de API manuais para acesso ao Gateway de IA público",
|
||||||
|
"virtualApiKeysTabIdentity": "Chaves de Identidade",
|
||||||
|
"virtualApiKeysTabVirtual": "Chaves Virtuais",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Chaves de Identidade para Todos os Usuários",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Todo usuário já possui uma chave de identidade Pangolin para esta organização. É exclusiva para a conta deles e os autentica em gateways de IA que eles podem acessar.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Exemplo",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Como os Usuários Obtêm Sua Chave",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Visite um URL de gateway de IA público no navegador e faça login com sua conta.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Ou vá para <url></url> enquanto estiver logado.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Você pode criar chaves de API virtuais adicionais na guia Chaves Virtuais. Essas chaves podem ser limitadas a gateways de IA públicos específicos e opcionalmente associadas a um usuário. Criação da chave concede acesso imediato aos gateways de IA públicos selecionados.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Criar manualmente uma chave",
|
||||||
|
"virtualApiKeysEmailIdentity": "Chaves de Identidade de E-mail",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Envie para cada usuário ou função selecionada a chave de identidade Pangolin deles.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Enviar para todos os usuários",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Envie um e-mail para cada membro da organização que tenha um e-mail de conta.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Utilizadores",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Papéis",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Enviar E-mails",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Chaves de identidade enviadas por e-mail",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Foram enviados {sent} e-mails.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} usuários foram pulados porque não têm endereço de e-mail.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Selecione pelo menos um usuário ou função, ou envie para todos os usuários.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Erro ao enviar chaves de identidade",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Falha ao enviar chaves de identidade por e-mail",
|
||||||
|
"virtualApiKeysBannerTitle": "Chaves de Identidade para Todos os Usuários",
|
||||||
|
"virtualApiKeysBannerDescription": "Todo usuário já tem uma chave de identidade disponível em {keysUrl}. Você também pode gerar manualmente chaves aqui que concedem acesso direto a gateways de IA públicos.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Ver Chaves de Identidade",
|
||||||
|
"virtualApiKeys": "Chaves de API Virtuais",
|
||||||
|
"virtualApiKeysSearch": "Procurar chaves...",
|
||||||
|
"virtualApiKeysCreate": "Criar Chave de API Virtual",
|
||||||
|
"virtualApiKeysCreateDescription": "Mintar uma chave manual que pode chamar gateways de IA públicos nesta organização",
|
||||||
|
"virtualApiKeysCreateButton": "Criar Chave",
|
||||||
|
"virtualApiKeysEmpty": "Ainda nenhuma chave de API virtual",
|
||||||
|
"virtualApiKeysName": "Nome",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Descrição (opcional)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Associar Usuário (opcional)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Associe esta chave a um usuário para rastrear o uso. Uma vez criada, esta chave concede acesso imediato aos gateways de IA públicos selecionados. Não é necessário associar um usuário manualmente ao recurso. A chave também aparecerá no perfil do usuário.",
|
||||||
|
"virtualApiKeysAllResources": "Todos os gateways de IA públicos",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Permitir que esta chave acesse todos os gateways de IA públicos da organização",
|
||||||
|
"virtualApiKeysSelectResources": "Gateways Públicos de IA",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Selecionar recursos",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Escolha quais gateways de IA públicos esta chave pode acessar",
|
||||||
|
"virtualApiKeysNoResources": "Nenhum recurso",
|
||||||
|
"virtualApiKeysSecret": "Chave",
|
||||||
|
"virtualApiKeysCopyKey": "Copie esta chave. Você pode visualizá-la novamente mais tarde na tabela ou ao editar.",
|
||||||
|
"virtualApiKeysViewSecret": "Ver Segredo",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Segredo da Chave de API Virtual",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Este segredo concede acesso aos gateways de IA públicos atribuídos a esta chave",
|
||||||
|
"virtualApiKeysEdit": "Editar Chave de API Virtual",
|
||||||
|
"virtualApiKeysEditDescription": "Atualize o usuário associado e o acesso ao gateway de IA público para esta chave",
|
||||||
|
"virtualApiKeysSaveButton": "Salvar mudanças",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Selecione pelo menos um gateway de IA público, ou habilite todos os gateways de IA públicos",
|
||||||
|
"virtualApiKeysUpdated": "Chave de API virtual atualizada",
|
||||||
|
"virtualApiKeysUpdatedDescription": "A chave de API virtual foi atualizada",
|
||||||
|
"virtualApiKeysErrorUpdate": "Erro ao atualizar chave de API virtual",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Falha ao atualizar chave de API virtual",
|
||||||
|
"virtualApiKeysErrorCreate": "Erro ao criar chave de API virtual",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Falha ao criar chave de API virtual",
|
||||||
|
"virtualApiKeysErrorDelete": "Erro ao excluir chave de API virtual",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Falha ao excluir chave de API virtual",
|
||||||
|
"virtualApiKeysDeleted": "Chave de API virtual excluída",
|
||||||
|
"virtualApiKeysDeletedDescription": "A chave de API virtual foi excluída",
|
||||||
|
"virtualApiKeysDelete": "Excluir Chave de API Virtual",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Excluir Chave",
|
||||||
|
"virtualApiKeysQuestionRemove": "Tem certeza que deseja excluir esta chave de API virtual?",
|
||||||
|
"virtualApiKeysMessageRemove": "Clientes usando esta chave perderão acesso imediatamente.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Erro ao carregar segredo",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Falha ao carregar segredo da chave de API virtual",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Não atribuído",
|
||||||
|
"virtualApiKeysInferenceBudget": "Orçamento",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Configure como esta chave restringe o uso de IA com base nos limites de gasto ou token",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Enviar chave por e-mail ao gerar",
|
||||||
|
"virtualApiKeysEmailThisKey": "Enviar esta chave por e-mail",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Envie a chave para o usuário associado e endereços adicionais após ser criada",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Envie a chave atual para o usuário associado e endereços adicionais",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "O e-mail não está configurado neste servidor",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Configurar SMTP para enviar chaves de API virtuais por e-mail.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Enviar para usuário associado",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Envie a chave para o e-mail da conta do usuário associado",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Associe um usuário para enviar a chave para esse usuário",
|
||||||
|
"virtualApiKeysEmailAdditional": "E-mails adicionais",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Adicionar e-mail e pressionar Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Selecione o usuário associado ou adicione pelo menos um endereço de e-mail",
|
||||||
|
"myVirtualApiKeysTitle": "Suas Chaves de API",
|
||||||
|
"myVirtualApiKeysDescription": "Veja sua chave de identidade e quaisquer chaves de API virtuais atribuídas a você nesta organização",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Suas Chaves de API para {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Veja sua chave de identidade e chaves de API virtuais atribuídas a você que podem acessar {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Chave de Identidade",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Sua Chave de API Pessoal",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Sua chave pessoal para esta organização. É exclusiva para sua conta e usada para identificá-lo ao chamar recursos do Gateway de IA.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Sua Chave de API Pessoal para {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Sua chave pessoal para esta organização. Use-a para chamar {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Chaves Atribuídas",
|
||||||
|
"myVirtualApiKeysManualDescription": "Chaves de API virtuais manuais associadas a sua conta",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Chaves de API virtuais manuais associadas à sua conta que podem acessar {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Ainda não há chaves atribuídas",
|
||||||
|
"myVirtualApiKeysKindUser": "Identidade",
|
||||||
|
"myVirtualApiKeysKindManual": "Manual",
|
||||||
|
"myVirtualApiKeysUnnamed": "Chave sem nome",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Revelar Segredo",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Este segredo o autentica em recursos do Gateway de IA",
|
||||||
|
"aiProvidersTitle": "Provedores de IA",
|
||||||
|
"aiProvidersDescription": "Conecte provedores de modelo para cargas de trabalho de IA nesta organização",
|
||||||
|
"aiProvidersBannerTitle": "Conectar Provedores de Modelo",
|
||||||
|
"aiProvidersBannerDescription": "Provedores são os backends de modelo que o Pangolin usa para cargas de trabalho de IA. Conecte OpenAI, Anthropic e outros provedores aqui, depois anexe os gateways de IA públicos para que os usuários possam chamar modelos com acesso baseado em identidade, orçamentos e logs.",
|
||||||
|
"aiProvidersAdd": "Adicionar Provedor",
|
||||||
|
"aiProvidersSearch": "Procurar provedores...",
|
||||||
|
"aiProvidersEmpty": "Ainda não há provedores de IA",
|
||||||
|
"aiProviderCreate": "Criar Provedor de IA",
|
||||||
|
"aiProviderCreateDescription": "Adicionar um provedor de modelo para esta organização",
|
||||||
|
"aiProviderSeeAll": "Ver Todos os Provedores",
|
||||||
|
"aiProviderSetting": "Configurações do Provedor para {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Configure este provedor de IA",
|
||||||
|
"aiProviderGeneral": "Gerais",
|
||||||
|
"aiProviderGeneralDescription": "Configurações básicas para este provedor",
|
||||||
|
"aiProviderConfiguration": "Configuração",
|
||||||
|
"aiProviderConfigurationDescription": "Roteamento de rede e autenticação para este provedor",
|
||||||
|
"aiProviderNetworkSettings": "Configurações de Rede",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Escolha como o tráfego chega a este provedor",
|
||||||
|
"aiProviderAuthSettings": "Autenticação",
|
||||||
|
"aiProviderAuthSettingsDescription": "Configure como este provedor autentica solicitações para seu URL upstream",
|
||||||
|
"aiProviderBudgetSettings": "Orçamento",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Configure como este provedor restringe o uso com base nos limites de gasto ou token",
|
||||||
|
"aiBudgetAdd": "Adicionar Orçamento",
|
||||||
|
"aiBudgetEmpty": "Nenhum orçamento configurado ainda. Clique em Adicionar Orçamento para definir um limite de gasto ou token.",
|
||||||
|
"aiBudgetUnit": "Tipo de Gasto",
|
||||||
|
"aiBudgetPeriod": "Período de Reinicialização",
|
||||||
|
"aiBudgetAmount": "Gasto Máximo",
|
||||||
|
"aiBudgetAmountPlaceholder": "Gasto máximo",
|
||||||
|
"aiBudgetPeriodHourly": "Por hora",
|
||||||
|
"aiBudgetPeriodDaily": "Diário",
|
||||||
|
"aiBudgetPeriodWeekly": "Semanal",
|
||||||
|
"aiBudgetPeriodMonthly": "Mensal",
|
||||||
|
"aiBudgetPeriodYearly": "Anual",
|
||||||
|
"aiBudgetPeriodLifetime": "Por toda a vida",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Tokens",
|
||||||
|
"aiBudgetConflictError": "Já existe um orçamento para este período de reinicialização e tipo de gasto",
|
||||||
|
"aiBudgetInvalidAmountError": "Digite um gasto máximo maior que 0",
|
||||||
|
"aiBudgetUpdated": "Orçamentos atualizados",
|
||||||
|
"aiBudgetErrorSave": "Falha ao atualizar orçamentos",
|
||||||
|
"aiProviderType": "Tipo de Provedor",
|
||||||
|
"aiProviderTypeSearch": "Procurar provedores...",
|
||||||
|
"aiProviderTypeNotFound": "Nenhum tipo de provedor encontrado",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Antropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Gateway de IA Vercel",
|
||||||
|
"aiProviderTypeCustom": "Personalizado",
|
||||||
|
"aiProviderTypeOpenaiDescription": "API OpenAI",
|
||||||
|
"aiProviderTypeAnthropicDescription": "API Antropic",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "API Google Gemini",
|
||||||
|
"aiProviderTypeVertexAiDescription": "API Google Vertex AI",
|
||||||
|
"aiProviderTypeBedrockDescription": "API Runtime do Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "API Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "API OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "API Gateway de IA Vercel",
|
||||||
|
"aiProviderTypeCustomDescription": "Traga seu próprio ponto de extremidade ou roteie através de alvos do site",
|
||||||
|
"aiProviderUpstreamUrl": "URL de Upstream",
|
||||||
|
"aiProviderUpstreamUrlDescription": "URL base para a API do provedor",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Deixe em branco para usar o URL de upstream padrão para este provedor",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "URL de Upstream Efetivo",
|
||||||
|
"aiProviderApiKey": "Chave API",
|
||||||
|
"aiProviderApiKeyDescription": "Chave API usada para autenticar pedidos a este provedor",
|
||||||
|
"aiProviderCustomHeadersDescription": "Cabeçalhos enviados em cada solicitação para este provedor. Separados por novas linhas: Nome-Do-Cabeçalho: valor",
|
||||||
|
"aiProviderApiKeyLastChars": "Chave API",
|
||||||
|
"aiProviderAuthType": "Tipo de Autenticação",
|
||||||
|
"aiProviderAuthTypeSearch": "Buscar tipos de autenticação...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Nenhum tipo de autenticação encontrado",
|
||||||
|
"aiProviderAuthTypeBearer": "Portador",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Autorização: Chave Portador. Usada pelo OpenAI e pela maioria dos provedores",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "Cabeçalho x-api-key. Usado por Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "Cabeçalho x-goog-api-key. Usado pelo Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Autorização: Chave Splunk. Usada pelo Coletor de Eventos HTTP Splunk",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Gateway AI da Cloudflare",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Chave Portador. Usada pelo Gateway AI da Cloudflare",
|
||||||
|
"aiProviderAuthTypeNone": "Sem Autenticação",
|
||||||
|
"aiProviderAuthTypePassthrough": "Transmissão",
|
||||||
|
"aiProviderAuthTypeDescription": "Como a API de upstream autentica os pedidos",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Encaminhar cabeçalhos da chave API do chamador para o upstream",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Não enviar cabeçalhos de autenticação para o upstream",
|
||||||
|
"aiProviderRoutingMode": "Modo de Roteamento",
|
||||||
|
"aiProviderRoutingModeDescription": "Enviar tráfego para um URL de upstream ou para alvos HTTP em seus sites",
|
||||||
|
"aiProviderRoutingModeUrl": "URL de Upstream",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Chamar uma URL base de API pública ou privada",
|
||||||
|
"aiProviderRoutingModeTarget": "Alvos do Site",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Roteie através de alvos em seus sites",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Após criar este provedor, configure alvos do site na aba Configurações de Rede.",
|
||||||
|
"aiProviderTargetNoOne": "Este provedor não tem alvos. Adicione um alvo para rotear pedidos pelos seus sites.",
|
||||||
|
"aiProviderSkipTlsVerification": "Pular Verificação TLS",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Desativar a verificação de certificado TLS para a conexão upstream",
|
||||||
|
"aiProviderBudget": "Orçamento",
|
||||||
|
"aiProviderBudgetDescription": "Orçamento opcional de despesas ou tokens para este provedor",
|
||||||
|
"aiProviderBudgetAmount": "Valor do Orçamento",
|
||||||
|
"aiProviderBudgetUnit": "Unidade de Orçamento",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Tokens",
|
||||||
|
"aiProviderEnabled": "Ativado",
|
||||||
|
"aiProviderEnabledDescription": "Desativar totalmente este provedor em todos os recursos",
|
||||||
|
"aiProviderErrorCreate": "Falha ao criar provedor de IA",
|
||||||
|
"aiProviderErrorUpdate": "Falha ao atualizar provedor de IA",
|
||||||
|
"aiProviderErrorDelete": "Falha ao apagar provedor de IA",
|
||||||
|
"aiProviderErrorLoad": "Falha ao carregar provedor de IA",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Digite um URL de upstream válido",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "URL de upstream é necessário para este provedor",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Tipo de autenticação é necessário",
|
||||||
|
"aiProviderErrorApiKeyRequired": "Chave API é necessária",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Roteamento de alvos do site está disponível somente para provedores personalizados",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Selecione pelo menos uma capacidade de API",
|
||||||
|
"aiProviderCapabilities": "Capacidades de API",
|
||||||
|
"aiProviderCapabilitiesDescription": "Selecione quais formatos de API este provedor pode manipular. Provedores conhecidos começam com padrões recomendados.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Selecione quais formatos de API este fornecedor personalizado pode lidar",
|
||||||
|
"aiProviderCapabilitiesSelect": "Selecionar capacidades",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Nenhuma capacidade encontrada",
|
||||||
|
"aiProviderCapabilitiesSearch": "Buscar capacidades...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Chat Completações",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Suporta /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "Respostas OpenAI",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Suporta /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Mensagens Antropicas",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Suporta /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gêmeos Gerar Conteúdo",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Suporta a API diretta do Gêmeos",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Modelo Bedrock Invocar",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Suporta InvocarModelo do Amazon Bedrock",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex Gerar Conteúdo",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Suporta o formato Vertex AI Gêmeos",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Predict Bruto do Vertex",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Suporta Vertex AI rawPredict para modelos Antropicos",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock Conversa",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Suporta API de Conversa da Amazon Bedrock",
|
||||||
|
"aiProviderCreated": "Fornecedor de IA criado",
|
||||||
|
"aiProviderUpdated": "Fornecedor de IA atualizado",
|
||||||
|
"aiProviderDeleted": "Fornecedor de IA excluído",
|
||||||
|
"aiProviderDelete": "Excluir Provedor",
|
||||||
|
"aiProviderDeleteConfirm": "Excluir o Provedor",
|
||||||
|
"aiProviderQuestionRemove": "Tem certeza de que deseja excluir este fornecedor de IA?",
|
||||||
|
"aiProviderMessageRemove": "Isso excluirá permanentemente o fornecedor e seus modelos e alvos. Isso não pode ser desfeito.",
|
||||||
|
"aiProviderErrorNoUpdate": "Fornecedor de IA não está disponível para atualização",
|
||||||
|
"aiProviderModels": "Modelos",
|
||||||
|
"aiProviderModelsDescription": "Defina listas de permissão e bloqueio para este provedor. As solicitações devem corresponder a uma entrada permitida e não devem corresponder a uma entrada bloqueada.",
|
||||||
|
"aiProviderCreateModelsDescription": "Escolha quais modelos este fornecedor pode oferecer. Uma lista de permissão vazia nega todo o tráfego. Você pode adicionar ou alterar modelos mais tarde.",
|
||||||
|
"aiProviderModelsPlaceholder": "Buscar modelos ou digitar uma chave personalizada",
|
||||||
|
"aiProviderModelsAllow": "Lista de Permissão",
|
||||||
|
"aiProviderModelsAllowDescription": "Modelos que podem ser usados através deste fornecedor. Vazia significa negar tudo.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Insira a chave do modelo",
|
||||||
|
"aiProviderModelsAllowEmpty": "Nenhum modelo adicionado ainda.",
|
||||||
|
"aiProviderModelsBlock": "Lista de Bloqueio",
|
||||||
|
"aiProviderModelsBlockDescription": "Modelos a serem negados, mesmo que correspondam a uma entrada permitida.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Insira a chave do modelo",
|
||||||
|
"aiProviderModelsBlockEmpty": "Nenhum modelo adicionado ainda.",
|
||||||
|
"aiProviderModelsAdd": "Adicionar Modelos",
|
||||||
|
"aiProviderModelsClearAll": "Limpar Tudo",
|
||||||
|
"aiProviderModelsAddCustom": "Adicionar \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Pressione Enter para adicionar esta chave de modelo personalizada.",
|
||||||
|
"aiProviderModelsAddBulk": "Adicionar {count} chaves personalizadas",
|
||||||
|
"aiProviderModelsAddBulkHint": "Pressione Enter para adicionar {count} chaves personalizadas.",
|
||||||
|
"aiProviderModelsAddOne": "Adicionar {key} agora",
|
||||||
|
"aiProviderModelsAddSelected": "Adicionar Selecionado",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} selecionados",
|
||||||
|
"aiProviderModelsSelectAll": "Selecionar tudo",
|
||||||
|
"aiProviderModelsClearSelected": "Limpar",
|
||||||
|
"aiProviderModelsBulkHint": "Selecione modelos conhecidos ou digite uma chave personalizada.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Nenhum modelo de catálogo correspondente.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Modelos Conhecidos",
|
||||||
|
"aiProviderModelsAllLabel": "Todos os modelos",
|
||||||
|
"aiProviderModelsAllPatternHint": "Curinga: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Permitir todos os modelos",
|
||||||
|
"aiProviderModelsAddAllBlock": "Bloquear todos os modelos",
|
||||||
|
"aiProviderModelsAddAllDescription": "Usa o curinga * para que toda chave de modelo corresponda.",
|
||||||
|
"aiProviderModelsViewMore": "Ver mais ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Ver menos",
|
||||||
|
"aiProviderModelsRemove": "Remover modelo",
|
||||||
|
"aiProviderModelsEditHint": "Clique para editar configurações de modelo",
|
||||||
|
"aiProviderModelsSourceCatalog": "Modelo de catálogo conhecido",
|
||||||
|
"aiProviderModelsSourceCustom": "Chave de modelo personalizada",
|
||||||
|
"aiProviderModelsSourcePattern": "Padrão curinga",
|
||||||
|
"aiProviderModelsSourceAll": "Corresponde a toda chave de modelo",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Orçamento configurado",
|
||||||
|
"aiProviderModelsEditTitle": "Editar Modelo",
|
||||||
|
"aiProviderModelsEditDescription": "Atualizar a chave do modelo ou configure seu orçamento.",
|
||||||
|
"aiProviderModelsBudgetTab": "Orçamento",
|
||||||
|
"aiProviderModelsKeyLabel": "Chave do Modelo",
|
||||||
|
"aiProviderModelsKeyRequired": "Insira uma chave de modelo",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Esta chave de modelo já está em uma lista",
|
||||||
|
"aiProviderModelsOverlapError": "Esses padrões não podem estar em ambas as listas: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modelos atualizados",
|
||||||
|
"aiProviderModelsErrorUpdate": "Falha ao atualizar modelos",
|
||||||
|
"aiResourceProviders": "Provedores",
|
||||||
|
"aiResourceProvidersDescription": "Escolha quais fornecedores de IA este gateway de IA pode usar",
|
||||||
|
"aiResourceProvidersHelp": "Anexe fornecedores e escolha herdar (usar as listas de cada fornecedor) ou selecionar (escolher uma lista de permissão para este recurso). Padrões de permissão que conflitam entre os fornecedores anexados não são permitidos.",
|
||||||
|
"aiResourceProvidersSelect": "Selecione fornecedores",
|
||||||
|
"aiResourceProvidersEmpty": "Nenhum fornecedor de IA encontrado",
|
||||||
|
"aiResourceProvidersNoneAttached": "Nenhum fornecedor anexado ainda.",
|
||||||
|
"aiResourceProvidersAdd": "Adicionar fornecedor",
|
||||||
|
"aiResourceProvidersRemove": "Remover fornecedor",
|
||||||
|
"aiResourceProviderToggleEnabled": "Ativar ou desativar este fornecedor no recurso",
|
||||||
|
"aiResourceProviderDisabled": "Desativado",
|
||||||
|
"aiResourceProvidersUpdated": "Provedores atualizados",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Falha ao atualizar provedores",
|
||||||
|
"aiResourceProviderEditDescription": "Escolha como os modelos deste fornecedor são expostos neste recurso.",
|
||||||
|
"viewProviderSettings": "Ver Configurações do Provedor",
|
||||||
|
"aiResourceProviderMode": "Modo de acesso",
|
||||||
|
"aiResourceProviderModeInherit": "Herdar",
|
||||||
|
"aiResourceProviderModeSelect": "Selecionar",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Selecionar · {count} modelos",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Use as listas de permissão e bloqueio deste provedor conforme configurado no mesmo.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Escolha um subconjunto dos modelos da lista de permissão deste fornecedor para este recurso.",
|
||||||
|
"aiResourceProviderAllowModels": "Lista de permissão",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Selecionar modelos",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Buscar modelos...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Nenhum modelo encontrado",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Somente modelos da lista de permissão deste fornecedor podem ser selecionados.",
|
||||||
|
"aiResourceAliasRequired": "Alias é exigido para gateways de IA",
|
||||||
|
"aiResourceDomainConfiguration": "Configuração do Domínio",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Escolha o domínio que os clientes usarão para acessar este gateway de IA.",
|
||||||
|
"aiUsageAnalyticsTitle": "Análise de Uso de IA",
|
||||||
|
"aiUsageAnalyticsDescription": "Analise custo de gateway de IA, uso de token e atividade entre fornecedores, recursos, funções e usuários",
|
||||||
|
"aiUsageTabOverview": "Visão geral",
|
||||||
|
"aiUsageTabProviders": "Provedores",
|
||||||
|
"aiUsageTabResources": "Recursos",
|
||||||
|
"aiUsageFilterProvider": "Provedor",
|
||||||
|
"aiUsageFilterModel": "Modelo",
|
||||||
|
"aiUsageFilterResource": "Recurso",
|
||||||
|
"aiUsageFilterRole": "Função",
|
||||||
|
"aiUsageFilterUser": "Usuário",
|
||||||
|
"aiUsageFilterAllProviders": "Todos os Provedores",
|
||||||
|
"aiUsageFilterAllModels": "Todos os Modelos",
|
||||||
|
"aiUsageFilterAllResources": "Todos os Recursos",
|
||||||
|
"aiUsageFilterAllRoles": "Todas as Funções",
|
||||||
|
"aiUsageFilterAllUsers": "Todos os Usuários",
|
||||||
|
"aiUsageFilterSearch": "Pesquisar...",
|
||||||
|
"aiUsageFilterNotFound": "Nenhuma opção encontrada",
|
||||||
|
"aiUsageResetFilters": "Redefinir Filtros",
|
||||||
|
"aiUsageRefresh": "Atualizar",
|
||||||
|
"aiUsageTokenTypePrompt": "Prompt",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Leitura de cache",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Gravação de cache",
|
||||||
|
"aiUsageTokenTypeCompletion": "Conclusão",
|
||||||
|
"aiUsageTokenTypeReasoning": "Raciocínio",
|
||||||
|
"aiUsageRequests": "Solicitações",
|
||||||
|
"aiUsageCost": "Custo",
|
||||||
|
"aiUsageTokens": "Tokens",
|
||||||
|
"aiUsageOther": "Outro",
|
||||||
|
"aiUsageTotalRequests": "Total de Solicitações",
|
||||||
|
"aiUsageTotalTokens": "Total de Tokens",
|
||||||
|
"aiUsageTotalCost": "Custo Total",
|
||||||
|
"aiUsageEstimated": "Estimado",
|
||||||
|
"aiUsageRequestVolume": "Volume de Solicitações",
|
||||||
|
"aiUsageTokenUsage": "Uso de Token",
|
||||||
|
"aiUsageModelCost": "Custo do Modelo",
|
||||||
|
"aiUsageModelTokens": "Tokens do Modelo",
|
||||||
|
"aiUsageTopModels": "Principais Modelos",
|
||||||
|
"aiUsageTopProviders": "Principais Provedores",
|
||||||
|
"aiUsageProviderCost": "Custo do Provedor",
|
||||||
|
"aiUsageProviderTokenUsage": "Uso de Token do Provedor",
|
||||||
|
"aiUsageTopResources": "Principais Recursos",
|
||||||
|
"aiUsageResourceCost": "Custo do Recurso",
|
||||||
|
"aiUsageResourceTokenUsage": "Uso de Token do Recurso",
|
||||||
|
"aiUsageResourceTypePublic": "Recurso Público",
|
||||||
|
"aiUsageResourceTypeSite": "Recurso Privado",
|
||||||
|
"aiUsageNoResource": "Sem recurso",
|
||||||
|
"aiUsageRolesTab": "Funções",
|
||||||
|
"aiUsageUsersTab": "Usuários",
|
||||||
|
"aiUsageTopRoles": "Principais Funções",
|
||||||
|
"aiUsageRoleCost": "Custo da Função",
|
||||||
|
"aiUsageRoleTokenUsage": "Uso de Token da Função",
|
||||||
|
"aiUsageTopUsers": "Principais Usuários",
|
||||||
|
"aiUsageUserCost": "Custo do Usuário",
|
||||||
|
"aiUsageUserTokenUsage": "Uso de Token do Usuário",
|
||||||
|
"aiUsageUnknownUser": "Usuário desconhecido",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Chaves de API Virtuais",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Chave de API Virtual",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Todas as Chaves de API Virtuais",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Principais Chaves de API Virtuais",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Custo da Chave de API Virtual",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Uso de Token da Chave de API Virtual",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Nenhuma chave de API virtual",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Chave não nomeada",
|
||||||
|
"aiUsageLoading": "Carregando...",
|
||||||
|
"aiUsageNoData": "Sem dados",
|
||||||
|
"resourceBudgetSettings": "Orçamento",
|
||||||
|
"resourceBudgetSettingsDescription": "Configure como este gateway de IA restringe o uso com base em gastos ou limites de tokens",
|
||||||
"sidebarApiKeys": "Chaves API",
|
"sidebarApiKeys": "Chaves API",
|
||||||
"sidebarProvisioning": "Provisionamento",
|
"sidebarProvisioning": "Provisionamento",
|
||||||
"sidebarSettings": "Configurações",
|
"sidebarSettings": "Configurações",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Sub-rede",
|
"subnetPlaceholder": "Sub-rede",
|
||||||
"addressDescription": "O endereço interno do cliente. Deve estar dentro da sub-rede da organização.",
|
"addressDescription": "O endereço interno do cliente. Deve estar dentro da sub-rede da organização.",
|
||||||
"selectSites": "Selecionar sites",
|
"selectSites": "Selecionar sites",
|
||||||
|
"selectResources": "Selecionar recursos",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# recurso} other {# recursos}}",
|
||||||
"selectLabels": "Selecionar etiquetas",
|
"selectLabels": "Selecionar etiquetas",
|
||||||
"sitesDescription": "O cliente terá conectividade com os sites selecionados",
|
"sitesDescription": "O cliente terá conectividade com os sites selecionados",
|
||||||
"clientInstallOlm": "Instalar Olm",
|
"clientInstallOlm": "Instalar Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "O tempo está em segundos",
|
"timeIsInSeconds": "O tempo está em segundos",
|
||||||
"requireDeviceApproval": "Exigir aprovação do dispositivo",
|
"requireDeviceApproval": "Exigir aprovação do dispositivo",
|
||||||
"requireDeviceApprovalDescription": "Usuários com esta função precisam de novos dispositivos aprovados por um administrador antes que eles possam se conectar e acessar recursos.",
|
"requireDeviceApprovalDescription": "Usuários com esta função precisam de novos dispositivos aprovados por um administrador antes que eles possam se conectar e acessar recursos.",
|
||||||
"sshSettings": "Configurações SSH",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "Gateway de IA",
|
||||||
"sshAccess": "Acesso SSH",
|
"sshAccess": "Acesso SSH",
|
||||||
"rdpSettings": "Configurações RDP",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "Configurações VNC",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "Servidor SSH",
|
"sshServer": "Servidor SSH",
|
||||||
"rdpServer": "Servidor RDP",
|
"rdpServer": "Servidor RDP",
|
||||||
"vncServer": "Servidor VNC",
|
"vncServer": "Servidor VNC",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "Gateway de IA",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Esquema",
|
"editInternalResourceDialogScheme": "Esquema",
|
||||||
"editInternalResourceDialogEnableSsl": "Ativar TLS",
|
"editInternalResourceDialogEnableSsl": "Ativar TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Permitir ICMP Ping",
|
"privateResourceAllowIcmpPing": "Permitir ICMP Ping",
|
||||||
"privateResourceNetworkAccess": "Acesso à Rede",
|
"privateResourceNetworkAccess": "Acesso à Rede",
|
||||||
"privateResourceNetworkAccessDescription": "Controlar o acesso à porta TCP/UDP e se o ICMP ping é permitido para este recurso.",
|
"privateResourceNetworkAccessDescription": "Controlar o acesso à porta TCP/UDP e se o ICMP ping é permitido para este recurso.",
|
||||||
"hostSettings": "Configurações do Host",
|
"hostSettings": "Host",
|
||||||
"cidrSettings": "Configurações CIDR",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Propriedades do Recurso",
|
"createInternalResourceDialogResourceProperties": "Propriedades do Recurso",
|
||||||
"createInternalResourceDialogName": "Nome",
|
"createInternalResourceDialogName": "Nome",
|
||||||
"createInternalResourceDialogSite": "Site",
|
"createInternalResourceDialogSite": "Site",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "Gateway de IA",
|
||||||
"scheme": "Esquema",
|
"scheme": "Esquema",
|
||||||
"createInternalResourceDialogScheme": "Esquema",
|
"createInternalResourceDialogScheme": "Esquema",
|
||||||
"createInternalResourceDialogEnableSsl": "Ativar TLS",
|
"createInternalResourceDialogEnableSsl": "Ativar TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Senha válida",
|
"validPassword": "Senha válida",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Chave de API Virtual Válida",
|
||||||
"view": "Visualizar",
|
"view": "Visualizar",
|
||||||
"configManaged": "Configuração Gerenciada",
|
"configManaged": "Configuração Gerenciada",
|
||||||
"connectedClient": "Cliente Conectado",
|
"connectedClient": "Cliente Conectado",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Retenção de Log",
|
"logRetention": "Retenção de Log",
|
||||||
"logRetentionDescription": "Gerenciar quanto tempo os diferentes tipos de logs são mantidos para esta organização ou desativá-los",
|
"logRetentionDescription": "Gerenciar quanto tempo os diferentes tipos de logs são mantidos para esta organização ou desativá-los",
|
||||||
"requestLogsDescription": "Ver registros de pedidos detalhados de recursos nesta organização",
|
"requestLogsDescription": "Ver registros de pedidos detalhados de recursos nesta organização",
|
||||||
|
"aiSessionLogs": "Registros de Sessão do Gateway de IA",
|
||||||
|
"aiSessionLogsDescription": "Veja as transcrições de prompt e resposta para solicitações de gateway de IA nesta organização",
|
||||||
|
"sidebarLogsAi": "Registros de Sessão",
|
||||||
|
"commandLogsAi": "Registros de Sessão",
|
||||||
|
"sidebarLogsAiUsage": "Análise de Uso",
|
||||||
|
"commandLogsAiUsage": "Análise de Uso",
|
||||||
|
"provider": "Provedor",
|
||||||
|
"capability": "Capacidade",
|
||||||
|
"model": "Modelo",
|
||||||
|
"virtualApiKey": "Chave de API Virtual",
|
||||||
|
"noVirtualApiKey": "Sem chave de API virtual",
|
||||||
|
"stream": "Stream",
|
||||||
|
"streaming": "Transmitindo",
|
||||||
|
"nonStreaming": "Não-streaming",
|
||||||
|
"statusCode": "Código de Status",
|
||||||
|
"aiSessionId": "ID de Sessão",
|
||||||
|
"aiSessionRequest": "Solicitação",
|
||||||
|
"aiSessionResponse": "Resposta",
|
||||||
|
"aiSessionNoData": "Nenhum dado capturado",
|
||||||
|
"aiSessionCouldNotParse": "(bruto, não foi possível analisar transcrição)",
|
||||||
|
"aiSessionLogTruncated": "Esta sessão foi truncada antes do armazenamento e pode estar incompleta.",
|
||||||
|
"aiSessionViewRaw": "Ver JSON Bruto",
|
||||||
|
"aiSessionViewChat": "Ver Chat",
|
||||||
|
"cost": "Custo",
|
||||||
|
"estimated": "Estimado",
|
||||||
|
"tokenUsage": "Uso de Token",
|
||||||
|
"promptTokens": "Tokens de Prompt",
|
||||||
|
"cacheReadTokens": "Tokens de Leitura de Cache",
|
||||||
|
"cacheWriteTokens": "Tokens de Gravação de Cache",
|
||||||
|
"completionTokens": "Tokens de Conclusão",
|
||||||
|
"reasoningTokens": "Tokens de Raciocínio",
|
||||||
|
"totalTokens": "Total de Tokens",
|
||||||
"requestAnalyticsDescription": "Exibir análise detalhada de pedidos para recursos nesta organização",
|
"requestAnalyticsDescription": "Exibir análise detalhada de pedidos para recursos nesta organização",
|
||||||
"logRetentionRequestLabel": "Retenção de Registro de Pedido HTTP",
|
"logRetentionRequestLabel": "Retenção de Registro de Pedido HTTP",
|
||||||
"logRetentionRequestDescription": "Por quanto tempo manter os registros de pedidos",
|
"logRetentionRequestDescription": "Por quanto tempo manter os registros de pedidos",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Por quanto tempo manter os registros de ação",
|
"logRetentionActionDescription": "Por quanto tempo manter os registros de ação",
|
||||||
"logRetentionConnectionLabel": "Retenção de registro de conexão",
|
"logRetentionConnectionLabel": "Retenção de registro de conexão",
|
||||||
"logRetentionConnectionDescription": "Por quanto tempo manter os registros de conexão",
|
"logRetentionConnectionDescription": "Por quanto tempo manter os registros de conexão",
|
||||||
|
"logRetentionAISessionsLabel": "Retenção de Registros de Sessão do Gateway de IA",
|
||||||
|
"logRetentionAISessionsDescription": "Quanto tempo reter logs de sessão de prompt/resposta do gateway de IA",
|
||||||
"logRetentionDisabled": "Desabilitado",
|
"logRetentionDisabled": "Desabilitado",
|
||||||
"logRetention3Days": "3 dias",
|
"logRetention3Days": "3 dias",
|
||||||
"logRetention7Days": "7 dias",
|
"logRetention7Days": "7 dias",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Endereço de origem",
|
"sourceAddress": "Endereço de origem",
|
||||||
"destinationAddress": "Endereço de destino",
|
"destinationAddress": "Endereço de destino",
|
||||||
"duration": "Duração",
|
"duration": "Duração",
|
||||||
"licenseRequiredToUse": "Uma licença <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> ou <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> é necessária para usar este recurso. <bookADemoLink>Reserve um teste de demonstração ou POC para saber mais.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Licença da Edição Empresarial</enterpriseEditionLink> é necessária. <bookADemoLink>Agende uma demo ou teste</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "O <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> é necessário para usar este recurso. Este recurso também está disponível no <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Reserve uma demonstração ou avaliação POC para saber mais.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Licença da Edição Empresarial</enterpriseEditionLink> é necessária. <bookADemoLink>Agende uma demo ou teste</bookADemoLink>",
|
||||||
"certResolver": "Resolvedor de Certificado",
|
"certResolver": "Resolvedor de Certificado",
|
||||||
"certResolverDescription": "Selecione o resolvedor de certificados para este recurso.",
|
"certResolverDescription": "Selecione o resolvedor de certificados para este recurso.",
|
||||||
"selectCertResolver": "Selecionar solucionador de certificado",
|
"selectCertResolver": "Selecionar solucionador de certificado",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Selecionar múltiplos sites permite roteamento resiliente e failover para alta disponibilidade.",
|
"internalResourceFormMultiSiteRoutingHelp": "Selecionar múltiplos sites permite roteamento resiliente e failover para alta disponibilidade.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Saiba mais",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Saiba mais",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Restrinja o acesso a portas TCP/UDP específicas ou permita/bloqueie todas as portas.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Restrinja o acesso a portas TCP/UDP específicas ou permita/bloqueie todas as portas.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "Configuração HTTP",
|
"createInternalResourceDialogHttpConfiguration": "Configuração do Domínio",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Escolha o domínio que os clientes usarão para acessar este recurso via HTTP ou HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Escolha o domínio que os clientes usarão para acessar este recurso via HTTP ou HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "Configuração HTTP",
|
"editInternalResourceDialogHttpConfiguration": "Configuração do Domínio",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Escolha o domínio que os clientes usarão para acessar este recurso via HTTP ou HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Escolha o domínio que os clientes usarão para acessar este recurso via HTTP ou HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Sem Etiqueta",
|
"resourceLauncherUnlabeled": "Sem Etiqueta",
|
||||||
|
"resourceLauncherAiGateway": "Gateway de IA",
|
||||||
"resourceLauncherNoSite": "Sem Site",
|
"resourceLauncherNoSite": "Sem Site",
|
||||||
|
"resourceLauncherAvailableModels": "Modelos Disponíveis",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Modelos que você pode usar com este gateway de IA.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Nenhum modelo está disponível para este recurso.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Não foi possível carregar modelos disponíveis.",
|
||||||
|
"resourceLauncherApiKeys": "Chaves de API",
|
||||||
|
"resourceLauncherApiKeysDescription": "Use sua chave de identidade ou uma chave atribuída para autenticar com este recurso.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Chave de Identidade",
|
||||||
|
"resourceLauncherApiKeysManual": "Chaves Atribuídas",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Nenhuma chave de API está disponível para este recurso.",
|
||||||
|
"resourceLauncherApiKeysError": "Não foi possível carregar chaves de API.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Nenhum recurso neste grupo",
|
"resourceLauncherNoResourcesInGroup": "Nenhum recurso neste grupo",
|
||||||
"resourceLauncherEmptyStateTitle": "Nenhum Recurso Disponível",
|
"resourceLauncherEmptyStateTitle": "Nenhum Recurso Disponível",
|
||||||
"resourceLauncherEmptyStateDescription": "Você não tem acesso a nenhum recurso ainda. Entre em contato com seu administrador para solicitar acesso.",
|
"resourceLauncherEmptyStateDescription": "Você não tem acesso a nenhum recurso ainda. Entre em contato com seu administrador para solicitar acesso.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Não foi possível excluir a visualização do lançador. Por favor, tente novamente.",
|
"resourceLauncherViewDeleteFailedDescription": "Não foi possível excluir a visualização do lançador. Por favor, tente novamente.",
|
||||||
"memberPortalPrevious": "Anterior",
|
"memberPortalPrevious": "Anterior",
|
||||||
"memberPortalNext": "Próximo",
|
"memberPortalNext": "Próximo",
|
||||||
"httpSettings": "Configurações HTTP",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "Configurações TCP",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "Configurações UDP",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Estabelecendo uma conexão segura…",
|
"sshConnectingDescription": "Estabelecendo uma conexão segura…",
|
||||||
"sshConnecting": "A conectar…",
|
"sshConnecting": "A conectar…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Убедитесь, что вы вошли под правильным пользователем.",
|
"inviteLoginUser": "Убедитесь, что вы вошли под правильным пользователем.",
|
||||||
"inviteErrorNoUser": "Извините, но похоже, что приглашение, к которому вы пытаетесь получить доступ, предназначено для несуществующего пользователя.",
|
"inviteErrorNoUser": "Извините, но похоже, что приглашение, к которому вы пытаетесь получить доступ, предназначено для несуществующего пользователя.",
|
||||||
"inviteCreateUser": "Сначала создайте аккаунт.",
|
"inviteCreateUser": "Сначала создайте аккаунт.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Приглашения могут быть приняты только внутренними учетными записями. Выйдите и войдите с вашим паролем для этого электронного адреса.",
|
||||||
|
"inviteLoginInternalOnly": "Приглашения требуют внутреннюю учетную запись с паролем. Создайте учетную запись или войдите с вашим паролем.",
|
||||||
"goHome": "На главную",
|
"goHome": "На главную",
|
||||||
"inviteLogInOtherUser": "Войти под другим пользователем",
|
"inviteLogInOtherUser": "Войти под другим пользователем",
|
||||||
"createAnAccount": "Создать учётную запись",
|
"createAnAccount": "Создать учётную запись",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Страна не найдена.",
|
"noCountryFound": "Страна не найдена.",
|
||||||
"siteSelectionDescription": "Этот сайт предоставит подключение к цели.",
|
"siteSelectionDescription": "Этот сайт предоставит подключение к цели.",
|
||||||
"resourceType": "Тип ресурса",
|
"resourceType": "Тип ресурса",
|
||||||
"resourceTypeDescription": "Это контролирует протокол ресурса и то, как он будет отображаться в браузере. Это нельзя изменить позже.",
|
"resourceTypeDescription": "Это контролирует протокол ресурса и не может быть изменено позже.",
|
||||||
|
"resourceTypeSearch": "Поиск типов ресурсов...",
|
||||||
|
"resourceTypeNotFound": "Тип ресурса не найден",
|
||||||
|
"resourceTypeHttpDescription": "Проксирование веб-трафика через HTTPS с использованием доменного имени",
|
||||||
|
"resourceTypeInferenceDescription": "Маршрутизация запросов ИИ через прикрепленных поставщиков",
|
||||||
|
"resourceTypeSshDescription": "Доступ к серверу SSH из браузера",
|
||||||
|
"resourceTypeRdpDescription": "Доступ к удаленному рабочему столу из браузера",
|
||||||
|
"resourceTypeVncDescription": "Доступ к рабочему столу VNC из браузера",
|
||||||
|
"resourceTypeTcpDescription": "Проксирование сырых TCP данных с использованием номера порта",
|
||||||
|
"resourceTypeUdpDescription": "Проксирование сырых UDP данных с использованием номера порта",
|
||||||
|
"privateResourceTypeDescription": "Это контролирует, как клиенты достигают ресурса. Это не может быть изменено позже.",
|
||||||
|
"privateResourceTypeHostDescription": "Опубликуйте один хост в сети сайта для подключенных клиентов",
|
||||||
|
"privateResourceTypeCidrDescription": "Опубликуйте диапазон CIDR в сети сайта для подключенных клиентов",
|
||||||
|
"privateResourceTypeHttpDescription": "Доступ к сервису HTTP или HTTPS через домен",
|
||||||
|
"privateResourceTypeSshDescription": "Доступ к серверу SSH от подключенных клиентов",
|
||||||
"resourceDomainDescription": "Ресурс будет предоставлен по этому полностью определенному доменному имени.",
|
"resourceDomainDescription": "Ресурс будет предоставлен по этому полностью определенному доменному имени.",
|
||||||
"resourceHTTPSSettings": "Настройки HTTPS",
|
"resourceHTTPSSettings": "Настройки HTTPS",
|
||||||
"resourceHTTPSSettingsDescription": "Настройка доступа к ресурсу по HTTPS",
|
"resourceHTTPSSettingsDescription": "Настройка доступа к ресурсу по HTTPS",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Сохранить настройки",
|
"authMethodsSave": "Сохранить настройки",
|
||||||
"policyAuthStackTitle": "Аутентификация",
|
"policyAuthStackTitle": "Аутентификация",
|
||||||
"policyAuthStackDescription": "Контроль, какие методы аутентификации требуются для доступа к этому ресурсу",
|
"policyAuthStackDescription": "Контроль, какие методы аутентификации требуются для доступа к этому ресурсу",
|
||||||
|
"policyAuthInferenceStackDescription": "Выберите, какие пользователи и роли могут аутентифицироваться на этом ИИ шлюзе",
|
||||||
"policyAuthOrLogicTitle": "Несколько методов аутентификации активны",
|
"policyAuthOrLogicTitle": "Несколько методов аутентификации активны",
|
||||||
"policyAuthOrLogicBanner": "Посетители могут аутентифицироваться, используя любой из активных методов ниже. Им не нужно выполнять все.",
|
"policyAuthOrLogicBanner": "Посетители могут аутентифицироваться, используя любой из активных методов ниже. Им не нужно выполнять все.",
|
||||||
"policyAuthMethodActive": "Активно",
|
"policyAuthMethodActive": "Активно",
|
||||||
"policyAuthMethodOff": "Отключено",
|
"policyAuthMethodOff": "Отключено",
|
||||||
"policyAuthSsoTitle": "Платформа SSO",
|
"policyAuthSsoTitle": "Платформа SSO",
|
||||||
"policyAuthSsoDescription": "Требуется войти через поставщика удостоверений вашей организации",
|
"policyAuthSsoDescription": "Требуется войти через поставщика удостоверений вашей организации",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "URL входа в систему",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "У каждого пользователя уже есть идентификационный ключ API, поэтому вам нужно создать виртуальные ключи API для не пользовательских клиентов или общего доступа.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Пользователи получают свой ключ здесь после входа в систему с провайдером идентификаторов.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} пользователей, {roles} ролей",
|
"policyAuthSsoSummary": "{idp} · {users} пользователей, {roles} ролей",
|
||||||
"policyAuthSsoDefaultIdp": "Поставщик по умолчанию",
|
"policyAuthSsoDefaultIdp": "Поставщик по умолчанию",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Добавить поставщика удостоверений по умолчанию",
|
"policyAuthAddDefaultIdentityProvider": "Добавить поставщика удостоверений по умолчанию",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Новая роль обязательна",
|
"accessRoleErrorNewRequired": "Новая роль обязательна",
|
||||||
"accessRoleErrorRemove": "Не удалось удалить роль",
|
"accessRoleErrorRemove": "Не удалось удалить роль",
|
||||||
"accessRoleErrorRemoveDescription": "Произошла ошибка при удалении роли.",
|
"accessRoleErrorRemoveDescription": "Произошла ошибка при удалении роли.",
|
||||||
|
"accessRoleInferenceBudget": "Бюджет ИИ",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Настройте, как члены этой роли ограничивают использование ИИ в зависимости от расходов или лимитов токенов",
|
||||||
"accessRoleName": "Название роли",
|
"accessRoleName": "Название роли",
|
||||||
"accessRoleQuestionRemove": "Вы собираетесь удалить `{name}` роль. Это действие нельзя отменить.",
|
"accessRoleQuestionRemove": "Вы собираетесь удалить `{name}` роль. Это действие нельзя отменить.",
|
||||||
"accessRoleRemove": "Удалить роль",
|
"accessRoleRemove": "Удалить роль",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Удалить сайт",
|
"actionDeleteSite": "Удалить сайт",
|
||||||
"actionGetSite": "Получить сайт",
|
"actionGetSite": "Получить сайт",
|
||||||
"actionListSites": "Список сайтов",
|
"actionListSites": "Список сайтов",
|
||||||
|
"actionCreateAiProvider": "Создать поставщика ИИ",
|
||||||
|
"actionDeleteAiProvider": "Удалить поставщика ИИ",
|
||||||
|
"actionGetAiProvider": "Get AI Provider",
|
||||||
|
"actionListAiProviders": "Список поставщиков ИИ",
|
||||||
|
"actionUpdateAiProvider": "Обновить поставщика ИИ",
|
||||||
|
"actionCreateAiModel": "Создать модель ИИ",
|
||||||
|
"actionDeleteAiModel": "Удалить модель ИИ",
|
||||||
|
"actionGetAiModel": "Получить модель ИИ",
|
||||||
|
"actionListAiModels": "Список моделей ИИ",
|
||||||
|
"actionUpdateAiModel": "Обновить модель ИИ",
|
||||||
|
"actionCreateAiBudget": "Создать бюджет ИИ",
|
||||||
|
"actionDeleteAiBudget": "Удалить бюджет ИИ",
|
||||||
|
"actionGetAiBudget": "Get AI Budget",
|
||||||
|
"actionListAiBudgets": "Список бюджетов ИИ",
|
||||||
|
"actionUpdateAiBudget": "Обновить бюджет ИИ",
|
||||||
|
"actionListResourceAiModels": "Список моделей ИИ ресурса",
|
||||||
|
"actionSetResourceAiModels": "Установить модели ИИ ресурса",
|
||||||
|
"actionCreateVirtualApiKey": "Создать виртуальный ключ API",
|
||||||
|
"actionDeleteVirtualApiKey": "Удалить виртуальный ключ API",
|
||||||
|
"actionGetVirtualApiKey": "Get Virtual API Key",
|
||||||
|
"actionListVirtualApiKeys": "Список виртуальных ключей API",
|
||||||
|
"actionUpdateVirtualApiKey": "Обновить виртуальный ключ API",
|
||||||
"actionApplyBlueprint": "Применить чертёж",
|
"actionApplyBlueprint": "Применить чертёж",
|
||||||
"actionListBlueprints": "Список чертежей",
|
"actionListBlueprints": "Список чертежей",
|
||||||
"actionGetBlueprint": "Получить чертёж",
|
"actionGetBlueprint": "Получить чертёж",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Установить ПИН-код ресурса",
|
"actionSetResourcePincode": "Установить ПИН-код ресурса",
|
||||||
"actionSetResourceEmailWhitelist": "Настроить белый список ресурсов email",
|
"actionSetResourceEmailWhitelist": "Настроить белый список ресурсов email",
|
||||||
"actionGetResourceEmailWhitelist": "Получить белый список ресурсов email",
|
"actionGetResourceEmailWhitelist": "Получить белый список ресурсов email",
|
||||||
|
"actionListResourcePolicies": "Список политик ресурсов",
|
||||||
|
"actionCreateResourcePolicy": "Создать политику ресурса",
|
||||||
"actionGetResourcePolicy": "Получить политику ресурса",
|
"actionGetResourcePolicy": "Получить политику ресурса",
|
||||||
"actionUpdateResourcePolicy": "Обновить политику ресурса",
|
"actionUpdateResourcePolicy": "Обновить политику ресурса",
|
||||||
|
"actionDeleteResourcePolicy": "Удалить политику ресурса",
|
||||||
"actionSetResourcePolicyUsers": "Установить пользователей политики ресурса",
|
"actionSetResourcePolicyUsers": "Установить пользователей политики ресурса",
|
||||||
"actionSetResourcePolicyRoles": "Установить роли политики ресурса",
|
"actionSetResourcePolicyRoles": "Установить роли политики ресурса",
|
||||||
"actionSetResourcePolicyPassword": "Установить пароль политики ресурса",
|
"actionSetResourcePolicyPassword": "Установить пароль политики ресурса",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Приглашения",
|
"sidebarInvitations": "Приглашения",
|
||||||
"sidebarRoles": "Роли",
|
"sidebarRoles": "Роли",
|
||||||
"sidebarShareableLinks": "Общие ссылки",
|
"sidebarShareableLinks": "Общие ссылки",
|
||||||
|
"sidebarAiGateway": "ИИ шлюз",
|
||||||
|
"sidebarAiProviders": "Поставщики",
|
||||||
|
"commandAiProviders": "Поставщики ИИ",
|
||||||
|
"sidebarVirtualApiKeys": "Виртуальные ключи API",
|
||||||
|
"sidebarMyApiKeys": "Ваши ключи API",
|
||||||
|
"sidebarAccount": "Пусковая установка",
|
||||||
|
"commandVirtualApiKeys": "Виртуальные ключи API",
|
||||||
|
"virtualApiKeysTitle": "Управление виртуальными ключами API",
|
||||||
|
"virtualApiKeysDescription": "Создание и управление ручными ключами API для доступа к ИИ шлюзам",
|
||||||
|
"virtualApiKeysTabIdentity": "Ключи Идентификации",
|
||||||
|
"virtualApiKeysTabVirtual": "Виртуальные ключи",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Ключи Идентификации для каждого пользователя",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Каждый пользователь уже имеет ключ для идентификации Pangolin для этой организации.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Пример",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Как пользователи получают свой ключ",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Перейдите по URL публичного ИИ шлюза в браузере и войдите в систему под своей учётной записью.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Или перейдите на <url></url>, находясь в системе.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Вы можете создать дополнительные виртуальные ключи API на вкладке Виртуальные ключи.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Создать ключ вручную",
|
||||||
|
"virtualApiKeysEmailIdentity": "Идентификационные ключи электронной почты",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Отправьте каждому выбранному пользователю или роли их идентификационный ключ Pangolin.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Отправить всем пользователям",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Отправить электронное письмо каждому члену организации, имеющему электронную почту.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Пользователи",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Роли",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "Отправить сообщения электронной почты",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Ключи идентификации отправлены по электронной почте",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Отправлено {sent} сообщений электронной почты.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} пользователей пропущено, поскольку у них нет адреса электронной почты.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "Выберите хотя бы одного пользователя или роль, или отправьте всем пользователям.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Ошибка при отправке ключей идентификации",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Не удалось отправить ключи идентификации по электронной почте",
|
||||||
|
"virtualApiKeysBannerTitle": "Ключи идентификации для каждого пользователя",
|
||||||
|
"virtualApiKeysBannerDescription": "Каждый пользователь уже имеет ключ идентификации, доступный на {keysUrl}.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Просмотр ключей идентификации",
|
||||||
|
"virtualApiKeys": "Виртуальные ключи API",
|
||||||
|
"virtualApiKeysSearch": "Поиск ключей...",
|
||||||
|
"virtualApiKeysCreate": "Создать виртуальный ключ API",
|
||||||
|
"virtualApiKeysCreateDescription": "Создать ручной ключ, который может вызвать публичные ИИ шлюзы в этой организации",
|
||||||
|
"virtualApiKeysCreateButton": "Создать ключ",
|
||||||
|
"virtualApiKeysEmpty": "Виртуальные ключи API еще не созданы",
|
||||||
|
"virtualApiKeysName": "Имя",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Описание (опционально)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Связать пользователя (опционально)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Свяжите этот ключ с пользователем для отслеживания использования.",
|
||||||
|
"virtualApiKeysAllResources": "Все публичные ИИ шлюзы",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Разрешите этому ключу доступ ко всем публичным ИИ шлюзам в организации",
|
||||||
|
"virtualApiKeysSelectResources": "Публичные ИИ шлюзы",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Выберите ресурсы",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Выберите, к каким публичным ИИ шлюзам этот ключ может получить доступ",
|
||||||
|
"virtualApiKeysNoResources": "Нет ресурсов",
|
||||||
|
"virtualApiKeysSecret": "Ключ",
|
||||||
|
"virtualApiKeysCopyKey": "Скопируйте этот ключ. Вы можете просмотреть его позже в таблице или при редактировании.",
|
||||||
|
"virtualApiKeysViewSecret": "Посмотреть секрет",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Секрет виртуального ключа API",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Этот секрет предоставляет доступ к назначенным публичным ИИ шлюзам этого ключа",
|
||||||
|
"virtualApiKeysEdit": "Изменить виртуальный ключ API",
|
||||||
|
"virtualApiKeysEditDescription": "Обновите связанного пользователя и доступ к публичным ИИ шлюзам для этого ключа",
|
||||||
|
"virtualApiKeysSaveButton": "Сохранить изменения",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "Выберите хотя бы один публичный ИИ шлюз или разрешите все публичные ИИ шлюзы",
|
||||||
|
"virtualApiKeysUpdated": "Виртуальный ключ API обновлен",
|
||||||
|
"virtualApiKeysUpdatedDescription": "Виртуальный ключ API обновлен",
|
||||||
|
"virtualApiKeysErrorUpdate": "Ошибка обновления виртуального ключа API",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Не удалось обновить виртуальный ключ API",
|
||||||
|
"virtualApiKeysErrorCreate": "Ошибка создания виртуального ключа API",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Не удалось создать виртуальный ключ API",
|
||||||
|
"virtualApiKeysErrorDelete": "Ошибка удаления виртуального ключа API",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Не удалось удалить виртуальный ключ API",
|
||||||
|
"virtualApiKeysDeleted": "Виртуальный ключ API удален",
|
||||||
|
"virtualApiKeysDeletedDescription": "Виртуальный ключ API удален",
|
||||||
|
"virtualApiKeysDelete": "Удалить виртуальный ключ API",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Удалить ключ",
|
||||||
|
"virtualApiKeysQuestionRemove": "Вы уверены, что хотите удалить этот виртуальный ключ API?",
|
||||||
|
"virtualApiKeysMessageRemove": "Клиенты, использующие этот ключ, немедленно потеряют доступ.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Ошибка загрузки секрета",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Не удалось загрузить секрет виртуального ключа API",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Не назначен",
|
||||||
|
"virtualApiKeysInferenceBudget": "Бюджет",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Настройте, как этот ключ ограничивает использование ИИ на основе расходов или лимитов токенов",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Отправить ключ по электронной почте при создании",
|
||||||
|
"virtualApiKeysEmailThisKey": "Отправить этот ключ по электронной почте",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Отправьте ключ связанному пользователю и дополнительным адресам после его создания",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Отправьте текущий ключ связанному пользователю и дополнительным адресам",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "На этом сервере не настроена электронная почта",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "Настройте SMTP для отправки ключей API по электронной почте.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "Отправить связанному пользователю",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Отправьте ключ на адрес электронной почты учетной записи связанного пользователя",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "Свяжите пользователя, чтобы отправить ему ключ",
|
||||||
|
"virtualApiKeysEmailAdditional": "Дополнительные электронные письма",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "Добавьте электронный адрес и нажмите Enter",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "Выберите связанного пользователя или добавьте хотя бы один адрес электронной почты",
|
||||||
|
"myVirtualApiKeysTitle": "Ваши ключи API",
|
||||||
|
"myVirtualApiKeysDescription": "Просмотрите ваш ключ идентификации и любые виртуальные ключи API, принадлежащие вам в этой организации",
|
||||||
|
"myVirtualApiKeysResourceTitle": "Ваши ключи API для {resourceName}",
|
||||||
|
"myVirtualApiKeysResourceDescription": "Просмотрите ваш ключ идентификации и виртуальные ключи API, принадлежащие вам, которые могут получить доступ к {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Ключ идентификации",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Ваш личный ключ API",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Ваш личный ключ для этой организации.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "Ваш личный ключ API для {resourceName}",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Ваш личный ключ для этой организации. Используйте его для вызова {resourceName}.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Назначенные ключи",
|
||||||
|
"myVirtualApiKeysManualDescription": "Ручные виртуальные ключи API, с которыми администратор связал вашу учетную запись",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "Ручные виртуальные ключи API, с которыми связана ваша учетная запись и которые могут получить доступ к {resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Назначенные ключи еще не созданы",
|
||||||
|
"myVirtualApiKeysKindUser": "Идентификация",
|
||||||
|
"myVirtualApiKeysKindManual": "Manual",
|
||||||
|
"myVirtualApiKeysUnnamed": "Неназванный ключ",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Показать секрет",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Этот секрет аутентифицирует вас в ресурсах ИИ шлюза",
|
||||||
|
"aiProvidersTitle": "Поставщики ИИ",
|
||||||
|
"aiProvidersDescription": "Подключите поставщиков моделей для ИИ нагрузок в этой организации",
|
||||||
|
"aiProvidersBannerTitle": "Подключить поставщиков моделей",
|
||||||
|
"aiProvidersBannerDescription": "Поставщики являются бэкэндами моделей, которые Pangolin использует для ИИ нагрузок.",
|
||||||
|
"aiProvidersAdd": "Добавить поставщика",
|
||||||
|
"aiProvidersSearch": "Поиск поставщиков...",
|
||||||
|
"aiProvidersEmpty": "Поставщики ИИ еще не созданы",
|
||||||
|
"aiProviderCreate": "Создать поставщика ИИ",
|
||||||
|
"aiProviderCreateDescription": "Добавьте поставщика моделей для этой организации",
|
||||||
|
"aiProviderSeeAll": "Посмотреть всех поставщиков",
|
||||||
|
"aiProviderSetting": "Настройки поставщика {providerName}",
|
||||||
|
"aiProviderSettingDescription": "Настройте этого поставщика ИИ",
|
||||||
|
"aiProviderGeneral": "Общие",
|
||||||
|
"aiProviderGeneralDescription": "Основные настройки для этого поставщика",
|
||||||
|
"aiProviderConfiguration": "Configuration",
|
||||||
|
"aiProviderConfigurationDescription": "Маршрутизация сети и аутентификация для этого поставщика",
|
||||||
|
"aiProviderNetworkSettings": "Настройки сети",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Выберите, как трафик достигает этого поставщика",
|
||||||
|
"aiProviderAuthSettings": "Аутентификация",
|
||||||
|
"aiProviderAuthSettingsDescription": "Настройте аутентификацию запросов этого поставщика к его URL-адресу",
|
||||||
|
"aiProviderBudgetSettings": "Бюджет",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Настройте, как этот поставщик ограничивает использование на основе расходов или лимитов токенов",
|
||||||
|
"aiBudgetAdd": "Добавить бюджет",
|
||||||
|
"aiBudgetEmpty": "Бюджеты еще не настроены. Нажмите «Добавить бюджет», чтобы установить лимит расходов или токенов.",
|
||||||
|
"aiBudgetUnit": "Тип расходов",
|
||||||
|
"aiBudgetPeriod": "Период сброса",
|
||||||
|
"aiBudgetAmount": "Максимальная трата",
|
||||||
|
"aiBudgetAmountPlaceholder": "Максимальная трата",
|
||||||
|
"aiBudgetPeriodHourly": "Почасово",
|
||||||
|
"aiBudgetPeriodDaily": "Ежедневно",
|
||||||
|
"aiBudgetPeriodWeekly": "Еженедельно",
|
||||||
|
"aiBudgetPeriodMonthly": "Ежемесячно",
|
||||||
|
"aiBudgetPeriodYearly": "Ежегодно",
|
||||||
|
"aiBudgetPeriodLifetime": "На протяжении жизни",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Токены",
|
||||||
|
"aiBudgetConflictError": "Бюджет для этого периода сброса и типа расходов уже существует",
|
||||||
|
"aiBudgetInvalidAmountError": "Введите максимальную трату, большую 0",
|
||||||
|
"aiBudgetUpdated": "Бюджеты обновлены",
|
||||||
|
"aiBudgetErrorSave": "Не удалось обновить бюджеты",
|
||||||
|
"aiProviderType": "Тип поставщика",
|
||||||
|
"aiProviderTypeSearch": "Поиск поставщиков...",
|
||||||
|
"aiProviderTypeNotFound": "Тип поставщика не найден",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Шлюз Vercel AI",
|
||||||
|
"aiProviderTypeCustom": "Пользовательский",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "API Google Vertex AI",
|
||||||
|
"aiProviderTypeBedrockDescription": "API Amazon Bedrock Runtime",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "API Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "API OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Шлюз Vercel AI API",
|
||||||
|
"aiProviderTypeCustomDescription": "Используйте свой собственный конечный узел или маршрутизируйте через целевые сайты",
|
||||||
|
"aiProviderUpstreamUrl": "Исходный URL",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Базовый URL для API провайдера",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Оставьте пустым, чтобы использовать исходный URL по умолчанию для этого провайдера",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Эффективный исходный URL",
|
||||||
|
"aiProviderApiKey": "API ключ",
|
||||||
|
"aiProviderApiKeyDescription": "API ключ, используемый для аутентификации запросов к этому провайдеру",
|
||||||
|
"aiProviderCustomHeadersDescription": "Заголовки, отправляемые в каждом запросе к этому провайдеру. Разделены новыми строками: Имя-заголовка: значение",
|
||||||
|
"aiProviderApiKeyLastChars": "API ключ",
|
||||||
|
"aiProviderAuthType": "Тип аутентификации",
|
||||||
|
"aiProviderAuthTypeSearch": "Поиск типов аутентификации...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Тип аутентификации не найден",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Authorization: Bearer key. Используется OpenAI и большинством провайдеров",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "Заголовок x-api-key. Используется компанией Anthropic",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "Заголовок x-goog-api-key. Используется компанией Google Gemini",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Authorization: Splunk key. Используется Splunk HTTP Event Collector",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer key. Используется Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeNone": "Без аутентификации",
|
||||||
|
"aiProviderAuthTypePassthrough": "Passthrough",
|
||||||
|
"aiProviderAuthTypeDescription": "Как исходный API аутентифицирует запросы",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Перенаправьте заголовки API ключей вызывающего на исходный сервер",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Не отправлять заголовки аутентификации на исходный сервер",
|
||||||
|
"aiProviderRoutingMode": "Режим маршрутизации",
|
||||||
|
"aiProviderRoutingModeDescription": "Отправка трафика на исходный URL или на HTTP-цели на ваших сайтах",
|
||||||
|
"aiProviderRoutingModeUrl": "Исходный URL",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Вызов общественного или частного базового URL-адреса API",
|
||||||
|
"aiProviderRoutingModeTarget": "Цели сайта",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Маршрутизация через цели на ваших сайтах",
|
||||||
|
"aiProviderRoutingModeTargetNote": "После создания этого провайдера настройте целевые сайты на вкладке Сетевые настройки.",
|
||||||
|
"aiProviderTargetNoOne": "У этого провайдера нет целей. Добавьте цель для маршрутизации запросов через ваши сайты.",
|
||||||
|
"aiProviderSkipTlsVerification": "Пропустить проверку TLS",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Отключить проверку сертификата TLS для исходного соединения",
|
||||||
|
"aiProviderBudget": "Бюджет",
|
||||||
|
"aiProviderBudgetDescription": "Дополнительные расходы или бюджет токенов для этого провайдера",
|
||||||
|
"aiProviderBudgetAmount": "Сумма бюджета",
|
||||||
|
"aiProviderBudgetUnit": "Единица бюджета",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Токены",
|
||||||
|
"aiProviderEnabled": "Включено",
|
||||||
|
"aiProviderEnabledDescription": "Полностью отключите этого провайдера на всех ресурсах",
|
||||||
|
"aiProviderErrorCreate": "Не удалось создать провайдера AI",
|
||||||
|
"aiProviderErrorUpdate": "Не удалось обновить провайдера AI",
|
||||||
|
"aiProviderErrorDelete": "Не удалось удалить провайдера AI",
|
||||||
|
"aiProviderErrorLoad": "Не удалось загрузить провайдера AI",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Введите действительный исходный URL",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Исходный URL обязателен для этого провайдера",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Тип аутентификации обязателен",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API ключ обязателен",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Маршрутизация целевых сайтов доступна только для пользовательских провайдеров",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "Выберите хотя бы одну возможность API",
|
||||||
|
"aiProviderCapabilities": "Возможности API",
|
||||||
|
"aiProviderCapabilitiesDescription": "Выберите, какие форматы API этот провайдер может обработать. Известные провайдеры начинают с рекомендованных по умолчанию.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Выберите, какие форматы API может обрабатывать этот пользовательский провайдер",
|
||||||
|
"aiProviderCapabilitiesSelect": "Выберите возможности",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Возможности не найдены",
|
||||||
|
"aiProviderCapabilitiesSearch": "Поиск возможностей...",
|
||||||
|
"aiCapabilityOpenaiChat": "Чат завершений OpenAI",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "Поддерживает /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "Ответы OpenAI",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "Поддерживает /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Сообщения Anthropic",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "Поддерживает /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini Создание контента",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Поддерживает прямой API Gemini",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Вызов модели Bedrock",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Поддерживает Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex Создание контента",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Поддерживает формат Vertex AI Gemini",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Сырой прогноз Vertex",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Поддерживает Vertex AI rawPredict для моделей Anthropic",
|
||||||
|
"aiCapabilityBedrockConverse": "Общение Bedrock",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Поддерживает API общения Amazon Bedrock",
|
||||||
|
"aiProviderCreated": "Провайдер AI создан",
|
||||||
|
"aiProviderUpdated": "Провайдер AI обновлен",
|
||||||
|
"aiProviderDeleted": "Провайдер AI удален",
|
||||||
|
"aiProviderDelete": "Удалить провайдера",
|
||||||
|
"aiProviderDeleteConfirm": "Удалить провайдера",
|
||||||
|
"aiProviderQuestionRemove": "Вы уверены, что хотите удалить этого провайдера AI?",
|
||||||
|
"aiProviderMessageRemove": "Это приведет к окончательному удалению провайдера и его моделей и целей. Это действие нельзя отменить.",
|
||||||
|
"aiProviderErrorNoUpdate": "Провайдер AI недоступен для обновления",
|
||||||
|
"aiProviderModels": "Модели",
|
||||||
|
"aiProviderModelsDescription": "Определите списки разрешений и блокировок для этого провайдера. Запросы должны соответствовать записи в списке разрешений и не должны соответствовать записи в списке блокировок.",
|
||||||
|
"aiProviderCreateModelsDescription": "Выберите, какие модели сможет обслуживать этот провайдер. Пустой список разрешений запрещает весь трафик. Вы можете добавлять или изменять модели позже.",
|
||||||
|
"aiProviderModelsPlaceholder": "Поиск моделей или введите пользовательский ключ",
|
||||||
|
"aiProviderModelsAllow": "Список разрешений",
|
||||||
|
"aiProviderModelsAllowDescription": "Модели, которые могут быть использованы через этого провайдера. Пустой список означает запретить все.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Введите ключ модели",
|
||||||
|
"aiProviderModelsAllowEmpty": "Пока модели не добавлены.",
|
||||||
|
"aiProviderModelsBlock": "Список блокировок",
|
||||||
|
"aiProviderModelsBlockDescription": "Модели для отклонения, даже если они соответствуют записи в списке разрешений.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Введите ключ модели",
|
||||||
|
"aiProviderModelsBlockEmpty": "Модели пока не добавлены.",
|
||||||
|
"aiProviderModelsAdd": "Добавить модели",
|
||||||
|
"aiProviderModelsClearAll": "Очистить все",
|
||||||
|
"aiProviderModelsAddCustom": "Добавить \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "Нажмите Enter, чтобы добавить этот пользовательский ключ модели.",
|
||||||
|
"aiProviderModelsAddBulk": "Добавить {count} пользовательских ключей",
|
||||||
|
"aiProviderModelsAddBulkHint": "Нажмите Enter, чтобы добавить {count} пользовательских ключей.",
|
||||||
|
"aiProviderModelsAddOne": "Добавить {key} сейчас",
|
||||||
|
"aiProviderModelsAddSelected": "Добавить выбранное",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} выбрано",
|
||||||
|
"aiProviderModelsSelectAll": "Выбрать все",
|
||||||
|
"aiProviderModelsClearSelected": "Очистить",
|
||||||
|
"aiProviderModelsBulkHint": "Выберите известные модели или введите пользовательский ключ.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Нет подходящих моделей каталога.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Известные модели",
|
||||||
|
"aiProviderModelsAllLabel": "Все модели",
|
||||||
|
"aiProviderModelsAllPatternHint": "Подстановочный знак: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Разрешить все модели",
|
||||||
|
"aiProviderModelsAddAllBlock": "Блокировать все модели",
|
||||||
|
"aiProviderModelsAddAllDescription": "Использует подстановочный знак *, чтобы каждый ключ модели соответствовал.",
|
||||||
|
"aiProviderModelsViewMore": "Просмотреть больше ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Просмотреть меньше",
|
||||||
|
"aiProviderModelsRemove": "Удалить модель",
|
||||||
|
"aiProviderModelsEditHint": "Нажмите, чтобы изменить параметры модели",
|
||||||
|
"aiProviderModelsSourceCatalog": "Известная модель каталога",
|
||||||
|
"aiProviderModelsSourceCustom": "Пользовательский ключ модели",
|
||||||
|
"aiProviderModelsSourcePattern": "Подстановочный шаблон",
|
||||||
|
"aiProviderModelsSourceAll": "Соответствует каждому ключу модели",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Бюджет настроен",
|
||||||
|
"aiProviderModelsEditTitle": "Изменить модель",
|
||||||
|
"aiProviderModelsEditDescription": "Обновите ключ модели или настройте его бюджет.",
|
||||||
|
"aiProviderModelsBudgetTab": "Бюджет",
|
||||||
|
"aiProviderModelsKeyLabel": "Ключ модели",
|
||||||
|
"aiProviderModelsKeyRequired": "Введите ключ модели",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Этот ключ модели уже находится в списке",
|
||||||
|
"aiProviderModelsOverlapError": "Эти шаблоны не могут находиться в обоих списках: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Модели обновлены",
|
||||||
|
"aiProviderModelsErrorUpdate": "Не удалось обновить модели",
|
||||||
|
"aiResourceProviders": "Провайдеры",
|
||||||
|
"aiResourceProvidersDescription": "Выберите, какие провайдеры AI может использовать этот шлюз AI",
|
||||||
|
"aiResourceProvidersHelp": "Присоединяйте провайдеров и выбирайте наследовать (использовать списки каждого провайдера) или выберите (выберите список разрешений для этого ресурса). Шаблоны разрешений, конфликтующие по присоединенным провайдерам, не допускаются.",
|
||||||
|
"aiResourceProvidersSelect": "Выберите провайдеров",
|
||||||
|
"aiResourceProvidersEmpty": "Провайдеры AI не найдены",
|
||||||
|
"aiResourceProvidersNoneAttached": "Пока провайдеры не прикреплены.",
|
||||||
|
"aiResourceProvidersAdd": "Добавить провайдера",
|
||||||
|
"aiResourceProvidersRemove": "Удалить провайдера",
|
||||||
|
"aiResourceProviderToggleEnabled": "Включить или отключить этот провайдер для ресурса",
|
||||||
|
"aiResourceProviderDisabled": "Отключено",
|
||||||
|
"aiResourceProvidersUpdated": "Провайдеры обновлены",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Не удалось обновить провайдеров",
|
||||||
|
"aiResourceProviderEditDescription": "Выберите, как модели этого провайдера будут выставлены на этом ресурсе.",
|
||||||
|
"viewProviderSettings": "Просмотреть настройки провайдера",
|
||||||
|
"aiResourceProviderMode": "Режим доступа",
|
||||||
|
"aiResourceProviderModeInherit": "Наследовать",
|
||||||
|
"aiResourceProviderModeSelect": "Выбрать",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Выбрать · {count} моделей",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Используйте разрешенные и блокирующие списки этого провайдера, как настроено на провайдере.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Выберите подмножество моделей из разрешенного списка этого провайдера для этого ресурса.",
|
||||||
|
"aiResourceProviderAllowModels": "Список разрешений",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Выберите модели",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Поиск моделей...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Модели не найдены",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Можно выбрать только модели из разрешенного списка этого провайдера.",
|
||||||
|
"aiResourceAliasRequired": "Для шлюзов AI требуется псевдоним",
|
||||||
|
"aiResourceDomainConfiguration": "Конфигурация домена",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Выберите домен, который клиенты будут использовать для доступа к этому шлюзу AI.",
|
||||||
|
"aiUsageAnalyticsTitle": "Аналитика использования AI",
|
||||||
|
"aiUsageAnalyticsDescription": "Анализируйте стоимость шлюза AI, использование токена и активность по провайдерам, ресурсам, ролям и пользователям",
|
||||||
|
"aiUsageTabOverview": "Обзор",
|
||||||
|
"aiUsageTabProviders": "Провайдеры",
|
||||||
|
"aiUsageTabResources": "Ресурсы",
|
||||||
|
"aiUsageFilterProvider": "Провайдер",
|
||||||
|
"aiUsageFilterModel": "Модель",
|
||||||
|
"aiUsageFilterResource": "Ресурс",
|
||||||
|
"aiUsageFilterRole": "Роль",
|
||||||
|
"aiUsageFilterUser": "Пользователь",
|
||||||
|
"aiUsageFilterAllProviders": "Все провайдеры",
|
||||||
|
"aiUsageFilterAllModels": "Все модели",
|
||||||
|
"aiUsageFilterAllResources": "Все ресурсы",
|
||||||
|
"aiUsageFilterAllRoles": "Все роли",
|
||||||
|
"aiUsageFilterAllUsers": "Все пользователи",
|
||||||
|
"aiUsageFilterSearch": "Поиск...",
|
||||||
|
"aiUsageFilterNotFound": "Опции не найдены",
|
||||||
|
"aiUsageResetFilters": "Сбросить фильтры",
|
||||||
|
"aiUsageRefresh": "Обновить",
|
||||||
|
"aiUsageTokenTypePrompt": "Подсказка",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Чтение кэша",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Запись в кэш",
|
||||||
|
"aiUsageTokenTypeCompletion": "Завершение",
|
||||||
|
"aiUsageTokenTypeReasoning": "Размышление",
|
||||||
|
"aiUsageRequests": "Запросы",
|
||||||
|
"aiUsageCost": "Стоимость",
|
||||||
|
"aiUsageTokens": "Токены",
|
||||||
|
"aiUsageOther": "Другое",
|
||||||
|
"aiUsageTotalRequests": "Всего запросов",
|
||||||
|
"aiUsageTotalTokens": "Всего токенов",
|
||||||
|
"aiUsageTotalCost": "Общая стоимость",
|
||||||
|
"aiUsageEstimated": "Оценочная",
|
||||||
|
"aiUsageRequestVolume": "Объем запросов",
|
||||||
|
"aiUsageTokenUsage": "Использование токенов",
|
||||||
|
"aiUsageModelCost": "Стоимость модели",
|
||||||
|
"aiUsageModelTokens": "Токены модели",
|
||||||
|
"aiUsageTopModels": "Лучшие модели",
|
||||||
|
"aiUsageTopProviders": "Лучшие провайдеры",
|
||||||
|
"aiUsageProviderCost": "Стоимость провайдера",
|
||||||
|
"aiUsageProviderTokenUsage": "Использование токенов провайдера",
|
||||||
|
"aiUsageTopResources": "Лучшие ресурсы",
|
||||||
|
"aiUsageResourceCost": "Стоимость ресурса",
|
||||||
|
"aiUsageResourceTokenUsage": "Использование токенов ресурса",
|
||||||
|
"aiUsageResourceTypePublic": "Общественный ресурс",
|
||||||
|
"aiUsageResourceTypeSite": "Частный ресурс",
|
||||||
|
"aiUsageNoResource": "Нет ресурса",
|
||||||
|
"aiUsageRolesTab": "Роли",
|
||||||
|
"aiUsageUsersTab": "Пользователи",
|
||||||
|
"aiUsageTopRoles": "Лучшие роли",
|
||||||
|
"aiUsageRoleCost": "Стоимость роли",
|
||||||
|
"aiUsageRoleTokenUsage": "Использование токенов роли",
|
||||||
|
"aiUsageTopUsers": "Лучшие пользователи",
|
||||||
|
"aiUsageUserCost": "Стоимость пользователя",
|
||||||
|
"aiUsageUserTokenUsage": "Использование токенов пользователя",
|
||||||
|
"aiUsageUnknownUser": "Неизвестный пользователь",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Виртуальные API ключи",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Виртуальный API ключ",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Все виртуальные API ключи",
|
||||||
|
"aiUsageTopVirtualApiKeys": "Лучшие виртуальные API ключи",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Стоимость виртуального API ключа",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Использование токенов виртуального API ключа",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Нет виртуального API ключа",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "Неименованный ключ",
|
||||||
|
"aiUsageLoading": "Загрузка...",
|
||||||
|
"aiUsageNoData": "Нет данных",
|
||||||
|
"resourceBudgetSettings": "Бюджет",
|
||||||
|
"resourceBudgetSettingsDescription": "Настройте, как этот шлюз AI ограничивает использование на основе затрат или лимитов токенов",
|
||||||
"sidebarApiKeys": "API ключи",
|
"sidebarApiKeys": "API ключи",
|
||||||
"sidebarProvisioning": "Подготовка",
|
"sidebarProvisioning": "Подготовка",
|
||||||
"sidebarSettings": "Настройки",
|
"sidebarSettings": "Настройки",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Подсеть",
|
"subnetPlaceholder": "Подсеть",
|
||||||
"addressDescription": "Внутренний адрес клиента. Должен находиться в подсети организации.",
|
"addressDescription": "Внутренний адрес клиента. Должен находиться в подсети организации.",
|
||||||
"selectSites": "Выберите сайты",
|
"selectSites": "Выберите сайты",
|
||||||
|
"selectResources": "Выберите ресурсы",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# ресурс} few {# ресурса} many {# ресурсов} other {# ресурсов}}",
|
||||||
"selectLabels": "Выберите метки",
|
"selectLabels": "Выберите метки",
|
||||||
"sitesDescription": "Клиент будет иметь подключение к выбранным сайтам",
|
"sitesDescription": "Клиент будет иметь подключение к выбранным сайтам",
|
||||||
"clientInstallOlm": "Установить Olm",
|
"clientInstallOlm": "Установить Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Время указано в секундах",
|
"timeIsInSeconds": "Время указано в секундах",
|
||||||
"requireDeviceApproval": "Требовать подтверждения устройства",
|
"requireDeviceApproval": "Требовать подтверждения устройства",
|
||||||
"requireDeviceApprovalDescription": "Пользователям с этой ролью нужны новые устройства, одобренные администратором, прежде чем они смогут подключаться и получать доступ к ресурсам.",
|
"requireDeviceApprovalDescription": "Пользователям с этой ролью нужны новые устройства, одобренные администратором, прежде чем они смогут подключаться и получать доступ к ресурсам.",
|
||||||
"sshSettings": "Настройки SSH",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI Шлюз",
|
||||||
"sshAccess": "Доступ по SSH",
|
"sshAccess": "Доступ по SSH",
|
||||||
"rdpSettings": "Настройки RDP",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "Настройки VNC",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH сервер",
|
"sshServer": "SSH сервер",
|
||||||
"rdpServer": "RDP сервер",
|
"rdpServer": "RDP сервер",
|
||||||
"vncServer": "VNC сервер",
|
"vncServer": "VNC сервер",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "СИДР",
|
"editInternalResourceDialogModeCidr": "СИДР",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI Шлюз",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Схема",
|
"editInternalResourceDialogScheme": "Схема",
|
||||||
"editInternalResourceDialogEnableSsl": "Включить TLS",
|
"editInternalResourceDialogEnableSsl": "Включить TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "Разрешить ICMP Ping",
|
"privateResourceAllowIcmpPing": "Разрешить ICMP Ping",
|
||||||
"privateResourceNetworkAccess": "Сетевой доступ",
|
"privateResourceNetworkAccess": "Сетевой доступ",
|
||||||
"privateResourceNetworkAccessDescription": "Управляйте доступом к портам TCP/UDP и настройте разрешение ICMP ping для данного ресурса.",
|
"privateResourceNetworkAccessDescription": "Управляйте доступом к портам TCP/UDP и настройте разрешение ICMP ping для данного ресурса.",
|
||||||
"hostSettings": "Настройки хоста",
|
"hostSettings": "Хост",
|
||||||
"cidrSettings": "Настройки CIDR",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Свойства ресурса",
|
"createInternalResourceDialogResourceProperties": "Свойства ресурса",
|
||||||
"createInternalResourceDialogName": "Имя",
|
"createInternalResourceDialogName": "Имя",
|
||||||
"createInternalResourceDialogSite": "Сайт",
|
"createInternalResourceDialogSite": "Сайт",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI Шлюз",
|
||||||
"scheme": "Схема",
|
"scheme": "Схема",
|
||||||
"createInternalResourceDialogScheme": "Схема",
|
"createInternalResourceDialogScheme": "Схема",
|
||||||
"createInternalResourceDialogEnableSsl": "Включить TLS",
|
"createInternalResourceDialogEnableSsl": "Включить TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Допустимый пароль",
|
"validPassword": "Допустимый пароль",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "Действительный виртуальный API ключ",
|
||||||
"view": "Просмотр",
|
"view": "Просмотр",
|
||||||
"configManaged": "Конфигурация управляется",
|
"configManaged": "Конфигурация управляется",
|
||||||
"connectedClient": "Подключенный клиент",
|
"connectedClient": "Подключенный клиент",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Сохранение журнала",
|
"logRetention": "Сохранение журнала",
|
||||||
"logRetentionDescription": "Управление сохранением различных типов журналов для этой организации или отключение их",
|
"logRetentionDescription": "Управление сохранением различных типов журналов для этой организации или отключение их",
|
||||||
"requestLogsDescription": "Просмотреть подробные журналы запроса ресурсов в этой организации",
|
"requestLogsDescription": "Просмотреть подробные журналы запроса ресурсов в этой организации",
|
||||||
|
"aiSessionLogs": "AI Логи сессии шлюза",
|
||||||
|
"aiSessionLogsDescription": "Просмотр транскриптов запросов и ответов для шлюзов AI в этой организации",
|
||||||
|
"sidebarLogsAi": "Логи сессий",
|
||||||
|
"commandLogsAi": "Логи сессий",
|
||||||
|
"sidebarLogsAiUsage": "Аналитика использования",
|
||||||
|
"commandLogsAiUsage": "Аналитика использования",
|
||||||
|
"provider": "Провайдер",
|
||||||
|
"capability": "Возможность",
|
||||||
|
"model": "Модель",
|
||||||
|
"virtualApiKey": "Виртуальный API ключ",
|
||||||
|
"noVirtualApiKey": "Нет виртуального API ключа",
|
||||||
|
"stream": "Поток",
|
||||||
|
"streaming": "Потоковая передача",
|
||||||
|
"nonStreaming": "Не потоковая",
|
||||||
|
"statusCode": "Код статуса",
|
||||||
|
"aiSessionId": "ID сессии",
|
||||||
|
"aiSessionRequest": "Запрос",
|
||||||
|
"aiSessionResponse": "Ответ",
|
||||||
|
"aiSessionNoData": "Данные не захвачены",
|
||||||
|
"aiSessionCouldNotParse": "(сырое, не удалось разобрать транскрипт)",
|
||||||
|
"aiSessionLogTruncated": "Эта сессия была усечена перед хранением и может быть неполной.",
|
||||||
|
"aiSessionViewRaw": "Просмотреть сырой JSON",
|
||||||
|
"aiSessionViewChat": "Просмотреть чат",
|
||||||
|
"cost": "Стоимость",
|
||||||
|
"estimated": "Оценочная",
|
||||||
|
"tokenUsage": "Использование токенов",
|
||||||
|
"promptTokens": "Токены подсказок",
|
||||||
|
"cacheReadTokens": "Токены чтения кэша",
|
||||||
|
"cacheWriteTokens": "Токены записи в кэш",
|
||||||
|
"completionTokens": "Токены завершения",
|
||||||
|
"reasoningTokens": "Токены размышления",
|
||||||
|
"totalTokens": "Всего токенов",
|
||||||
"requestAnalyticsDescription": "Просмотреть подробную аналитику запроса для ресурсов в этой организации",
|
"requestAnalyticsDescription": "Просмотреть подробную аналитику запроса для ресурсов в этой организации",
|
||||||
"logRetentionRequestLabel": "Сохранение HTTP Запросов Лога",
|
"logRetentionRequestLabel": "Сохранение HTTP Запросов Лога",
|
||||||
"logRetentionRequestDescription": "Как долго сохранять журналы запросов",
|
"logRetentionRequestDescription": "Как долго сохранять журналы запросов",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Как долго хранить журналы действий",
|
"logRetentionActionDescription": "Как долго хранить журналы действий",
|
||||||
"logRetentionConnectionLabel": "Сохранение журнала подключений",
|
"logRetentionConnectionLabel": "Сохранение журнала подключений",
|
||||||
"logRetentionConnectionDescription": "Как долго хранить журналы подключений",
|
"logRetentionConnectionDescription": "Как долго хранить журналы подключений",
|
||||||
|
"logRetentionAISessionsLabel": "Сохранение логов сессий AI шлюза",
|
||||||
|
"logRetentionAISessionsDescription": "Как долго сохранять логи сессий запросов/ответов шлюза AI",
|
||||||
"logRetentionDisabled": "Отключено",
|
"logRetentionDisabled": "Отключено",
|
||||||
"logRetention3Days": "3 дня",
|
"logRetention3Days": "3 дня",
|
||||||
"logRetention7Days": "7 дней",
|
"logRetention7Days": "7 дней",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Адрес источника",
|
"sourceAddress": "Адрес источника",
|
||||||
"destinationAddress": "Адрес назначения",
|
"destinationAddress": "Адрес назначения",
|
||||||
"duration": "Продолжительность",
|
"duration": "Продолжительность",
|
||||||
"licenseRequiredToUse": "Требуется лицензия на <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> для использования этой функции. <bookADemoLink>Забронируйте демонстрацию или пробный POC, чтобы узнать больше.</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Лицензия Enterprise Edition</enterpriseEditionLink> требуется. <bookADemoLink>Записаться на демо или пробную версию</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> требуется для использования этой функции. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Забронируйте демонстрацию или пробный POC, чтобы узнать больше.</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Лицензия Enterprise Edition</enterpriseEditionLink> требуется. <bookADemoLink>Записаться на демо или пробную версию</bookADemoLink>",
|
||||||
"certResolver": "Резольвер сертификата",
|
"certResolver": "Резольвер сертификата",
|
||||||
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
|
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
|
||||||
"selectCertResolver": "Выберите резолвер сертификата",
|
"selectCertResolver": "Выберите резолвер сертификата",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Выбор нескольких сайтов позволяет обеспечить отказоустойчивую маршрутизацию и фейловер для высокой доступности.",
|
"internalResourceFormMultiSiteRoutingHelp": "Выбор нескольких сайтов позволяет обеспечить отказоустойчивую маршрутизацию и фейловер для высокой доступности.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Узнать больше",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Узнать больше",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Ограничьте доступ к определенным TCP/UDP-портам или разрешите/заблокируйте все порты.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Ограничьте доступ к определенным TCP/UDP-портам или разрешите/заблокируйте все порты.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "Конфигурация HTTP",
|
"createInternalResourceDialogHttpConfiguration": "Конфигурация домена",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "Выберите домен, который клиенты будут использовать для доступа к этому ресурсу через HTTP или HTTPS.",
|
"createInternalResourceDialogHttpConfigurationDescription": "Выберите домен, который клиенты будут использовать для доступа к этому ресурсу через HTTP или HTTPS.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "Конфигурация HTTP",
|
"editInternalResourceDialogHttpConfiguration": "Конфигурация домена",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "Выберите домен, который клиенты будут использовать для доступа к этому ресурсу через HTTP или HTTPS.",
|
"editInternalResourceDialogHttpConfigurationDescription": "Выберите домен, который клиенты будут использовать для доступа к этому ресурсу через HTTP или HTTPS.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Без меток",
|
"resourceLauncherUnlabeled": "Без меток",
|
||||||
|
"resourceLauncherAiGateway": "AI Шлюз",
|
||||||
"resourceLauncherNoSite": "Без сайта",
|
"resourceLauncherNoSite": "Без сайта",
|
||||||
|
"resourceLauncherAvailableModels": "Доступные модели",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Модели, которые вы можете использовать с этим шлюзом AI.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Нет доступных моделей для этого ресурса.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Не удалось загрузить доступные модели.",
|
||||||
|
"resourceLauncherApiKeys": "API ключи",
|
||||||
|
"resourceLauncherApiKeysDescription": "Используйте свой идентификационный ключ или атрибутивный ключ для аутентификации с этим ресурсом.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Идентификационный ключ",
|
||||||
|
"resourceLauncherApiKeysManual": "Атрибутные ключи",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Нет доступных API ключей для этого ресурса.",
|
||||||
|
"resourceLauncherApiKeysError": "Не удалось загрузить API ключи.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Нет ресурсов в данной группе",
|
"resourceLauncherNoResourcesInGroup": "Нет ресурсов в данной группе",
|
||||||
"resourceLauncherEmptyStateTitle": "Нет доступных ресурсов",
|
"resourceLauncherEmptyStateTitle": "Нет доступных ресурсов",
|
||||||
"resourceLauncherEmptyStateDescription": "У вас пока нет доступа ни к одному ресурсу. Обратитесь к администратору, чтобы запросить доступ.",
|
"resourceLauncherEmptyStateDescription": "У вас пока нет доступа ни к одному ресурсу. Обратитесь к администратору, чтобы запросить доступ.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Не удалось удалить вид. Пожалуйста, попробуйте еще раз.",
|
"resourceLauncherViewDeleteFailedDescription": "Не удалось удалить вид. Пожалуйста, попробуйте еще раз.",
|
||||||
"memberPortalPrevious": "Предыдущий",
|
"memberPortalPrevious": "Предыдущий",
|
||||||
"memberPortalNext": "Следующий",
|
"memberPortalNext": "Следующий",
|
||||||
"httpSettings": "Настройки HTTP",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "Настройки TCP",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "Настройки UDP",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Установление защищенного соединения…",
|
"sshConnectingDescription": "Установление защищенного соединения…",
|
||||||
"sshConnecting": "Подключение…",
|
"sshConnecting": "Подключение…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "Lütfen doğru kullanıcı olarak oturum açtığınızdan emin olun.",
|
"inviteLoginUser": "Lütfen doğru kullanıcı olarak oturum açtığınızdan emin olun.",
|
||||||
"inviteErrorNoUser": "Üzgünüz, ancak erişmeye çalıştığınız davet, var olan bir kullanıcı için değil gibi görünüyor.",
|
"inviteErrorNoUser": "Üzgünüz, ancak erişmeye çalıştığınız davet, var olan bir kullanıcı için değil gibi görünüyor.",
|
||||||
"inviteCreateUser": "Öncelikle bir hesap oluşturun.",
|
"inviteCreateUser": "Öncelikle bir hesap oluşturun.",
|
||||||
|
"inviteErrorOidcNotAllowed": "Davetler yalnızca dahili hesaplar tarafından kabul edilebilir. Oturumunuzu kapatın ve bu e-postayla parolanızla giriş yapın.",
|
||||||
|
"inviteLoginInternalOnly": "Davet, parolası olan bir dahili hesap gerektirir. Hesap oluşturun veya parolanızla giriş yapın.",
|
||||||
"goHome": "Ana Sayfaya Dön",
|
"goHome": "Ana Sayfaya Dön",
|
||||||
"inviteLogInOtherUser": "Başka bir kullanıcı olarak giriş yapın",
|
"inviteLogInOtherUser": "Başka bir kullanıcı olarak giriş yapın",
|
||||||
"createAnAccount": "Bir Hesap Oluşturun",
|
"createAnAccount": "Bir Hesap Oluşturun",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "Ülke bulunamadı.",
|
"noCountryFound": "Ülke bulunamadı.",
|
||||||
"siteSelectionDescription": "Bu site hedefe bağlantı sağlayacaktır.",
|
"siteSelectionDescription": "Bu site hedefe bağlantı sağlayacaktır.",
|
||||||
"resourceType": "Kaynak Türü",
|
"resourceType": "Kaynak Türü",
|
||||||
"resourceTypeDescription": "Bu, kaynak protokolünü ve tarayıcıda nasıl görüntüleneceğini kontrol eder. Bu daha sonra değiştirilemez.",
|
"resourceTypeDescription": "Bu, kaynak protokolünü kontrol eder ve daha sonra değiştirilemez.",
|
||||||
|
"resourceTypeSearch": "Kaynak türlerini ara...",
|
||||||
|
"resourceTypeNotFound": "Hiçbir kaynak türü bulunamadı",
|
||||||
|
"resourceTypeHttpDescription": "Bir alan adı kullanarak HTTPS üzerinden web trafiğini proxy yapın",
|
||||||
|
"resourceTypeInferenceDescription": "Bağlı sağlayıcılar üzerinden AI isteklerini yönlendirin",
|
||||||
|
"resourceTypeSshDescription": "Tarayıcıdan bir SSH sunucusuna erişim",
|
||||||
|
"resourceTypeRdpDescription": "Tarayıcıdan bir uzaktan masaüstüne erişim",
|
||||||
|
"resourceTypeVncDescription": "Tarayıcıdan bir VNC masaüstüne erişim",
|
||||||
|
"resourceTypeTcpDescription": "Bir bağlantı noktası numarası kullanarak ham TCP trafiğini proxy yapın",
|
||||||
|
"resourceTypeUdpDescription": "Bir bağlantı noktası numarası kullanarak ham UDP trafiğini proxy yapın",
|
||||||
|
"privateResourceTypeDescription": "Bu, müşterilerin kaynağa nasıl erişeceğini kontrol eder. Daha sonra değiştirilemez.",
|
||||||
|
"privateResourceTypeHostDescription": "Bağlanan müşterilere site ağında tek bir ana makineyi açığa çıkartın",
|
||||||
|
"privateResourceTypeCidrDescription": "Bağlanan müşterilere site ağında bir CIDR aralığını açığa çıkartın",
|
||||||
|
"privateResourceTypeHttpDescription": "Bir alan adı üzerinden HTTP veya HTTPS hizmetine eriş",
|
||||||
|
"privateResourceTypeSshDescription": "Bağlanan müşteriler için bir SSH sunucusuna eriş",
|
||||||
"resourceDomainDescription": "Kaynak bu tam etki alanı adıyla sunulacak.",
|
"resourceDomainDescription": "Kaynak bu tam etki alanı adıyla sunulacak.",
|
||||||
"resourceHTTPSSettings": "HTTPS Ayarları",
|
"resourceHTTPSSettings": "HTTPS Ayarları",
|
||||||
"resourceHTTPSSettingsDescription": "Kaynağınıza HTTPS üzerinden erişimin nasıl sağlanacağını yapılandırın",
|
"resourceHTTPSSettingsDescription": "Kaynağınıza HTTPS üzerinden erişimin nasıl sağlanacağını yapılandırın",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "Ayarları Kaydet",
|
"authMethodsSave": "Ayarları Kaydet",
|
||||||
"policyAuthStackTitle": "Kimlik Doğrulama",
|
"policyAuthStackTitle": "Kimlik Doğrulama",
|
||||||
"policyAuthStackDescription": "Bu kaynağa erişim için hangi kimlik doğrulama yöntemlerinin gerekli olduğuna karar verin",
|
"policyAuthStackDescription": "Bu kaynağa erişim için hangi kimlik doğrulama yöntemlerinin gerekli olduğuna karar verin",
|
||||||
|
"policyAuthInferenceStackDescription": "Bu AI geçidine kimlerin ve hangi rollerin kimlik doğrulama yapabileceğini seçin",
|
||||||
"policyAuthOrLogicTitle": "Birden fazla kimlik doğrulama yöntemi etkin",
|
"policyAuthOrLogicTitle": "Birden fazla kimlik doğrulama yöntemi etkin",
|
||||||
"policyAuthOrLogicBanner": "Ziyaretçiler aşağıdaki etkin yöntemlerden herhangi birini kullanarak kimlik doğrulaması yapabilirler. Hepsini tamamlamaları gerekmez.",
|
"policyAuthOrLogicBanner": "Ziyaretçiler aşağıdaki etkin yöntemlerden herhangi birini kullanarak kimlik doğrulaması yapabilirler. Hepsini tamamlamaları gerekmez.",
|
||||||
"policyAuthMethodActive": "Etkin",
|
"policyAuthMethodActive": "Etkin",
|
||||||
"policyAuthMethodOff": "Kapalı",
|
"policyAuthMethodOff": "Kapalı",
|
||||||
"policyAuthSsoTitle": "Platform SSO",
|
"policyAuthSsoTitle": "Platform SSO",
|
||||||
"policyAuthSsoDescription": "Organizasyonunuzun kimlik sağlayıcısı üzerinden oturum açmayı zorunlu kılın",
|
"policyAuthSsoDescription": "Organizasyonunuzun kimlik sağlayıcısı üzerinden oturum açmayı zorunlu kılın",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "Giriş URL'si",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "Her kullanıcının zaten bir kimlik API anahtarı vardır, bu nedenle yalnızca kullanıcı olmayan istemciler veya ortak erişim için sanal API anahtarları oluşturmanız gerekir. Kullanıcılar bu kaynağın URL'si üzerinden kimlik sağlayıcılarıyla oturum açarak anahtarlarını alabilirler, bu girişten sonra gösterilecektir.",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "Her kullanıcının zaten bir kimlik API anahtarı vardır, bu nedenle yalnızca kullanıcı olmayan istemciler veya ortak erişim için sanal API anahtarları oluşturmanız gerekir. Kullanıcılar burada kimlik sağlayıcılarıyla giriş yaptıktan sonra anahtarlarını alırlar.",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} kullanıcısı, {roles} rolü",
|
"policyAuthSsoSummary": "{idp} · {users} kullanıcısı, {roles} rolü",
|
||||||
"policyAuthSsoDefaultIdp": "Varsayılan sağlayıcı",
|
"policyAuthSsoDefaultIdp": "Varsayılan sağlayıcı",
|
||||||
"policyAuthAddDefaultIdentityProvider": "Varsayılan Kimlik Sağlayıcı Ekle",
|
"policyAuthAddDefaultIdentityProvider": "Varsayılan Kimlik Sağlayıcı Ekle",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "Yeni rol gerekli",
|
"accessRoleErrorNewRequired": "Yeni rol gerekli",
|
||||||
"accessRoleErrorRemove": "Rol kaldırılamadı",
|
"accessRoleErrorRemove": "Rol kaldırılamadı",
|
||||||
"accessRoleErrorRemoveDescription": "Rol kaldırılırken bir hata oluştu.",
|
"accessRoleErrorRemoveDescription": "Rol kaldırılırken bir hata oluştu.",
|
||||||
|
"accessRoleInferenceBudget": "AI Bütçesi",
|
||||||
|
"accessRoleInferenceBudgetDescription": "Bu role ait üyelerin harcama veya jeton sınırlarına dayalı AI kullanımını nasıl kısıtlayacaklarını yapılandırın",
|
||||||
"accessRoleName": "Rol Adı",
|
"accessRoleName": "Rol Adı",
|
||||||
"accessRoleQuestionRemove": "{name} rolünü silmek üzeresiniz. Bu eylemi geri alamazsınız.",
|
"accessRoleQuestionRemove": "{name} rolünü silmek üzeresiniz. Bu eylemi geri alamazsınız.",
|
||||||
"accessRoleRemove": "Rolü Kaldır",
|
"accessRoleRemove": "Rolü Kaldır",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "Siteyi Sil",
|
"actionDeleteSite": "Siteyi Sil",
|
||||||
"actionGetSite": "Siteyi Al",
|
"actionGetSite": "Siteyi Al",
|
||||||
"actionListSites": "Siteleri Listele",
|
"actionListSites": "Siteleri Listele",
|
||||||
|
"actionCreateAiProvider": "AI Sağlayıcı Oluştur",
|
||||||
|
"actionDeleteAiProvider": "AI Sağlayıcı Sil",
|
||||||
|
"actionGetAiProvider": "AI Sağlayıcı Al",
|
||||||
|
"actionListAiProviders": "AI Sağlayıcıları Listele",
|
||||||
|
"actionUpdateAiProvider": "AI Sağlayıcı Güncelle",
|
||||||
|
"actionCreateAiModel": "AI Modeli Oluştur",
|
||||||
|
"actionDeleteAiModel": "AI Modeli Sil",
|
||||||
|
"actionGetAiModel": "AI Modeli Al",
|
||||||
|
"actionListAiModels": "AI Modellerini Listele",
|
||||||
|
"actionUpdateAiModel": "AI Modelini Güncelle",
|
||||||
|
"actionCreateAiBudget": "AI Bütçesi Oluştur",
|
||||||
|
"actionDeleteAiBudget": "AI Bütçesini Sil",
|
||||||
|
"actionGetAiBudget": "AI Bütçesi Al",
|
||||||
|
"actionListAiBudgets": "AI Bütçelerini Listele",
|
||||||
|
"actionUpdateAiBudget": "AI Bütçesi Güncelle",
|
||||||
|
"actionListResourceAiModels": "Kaynak AI Modellerini Listele",
|
||||||
|
"actionSetResourceAiModels": "Kaynak AI Modellerini Ayarla",
|
||||||
|
"actionCreateVirtualApiKey": "Sanal API Anahtarı Oluştur",
|
||||||
|
"actionDeleteVirtualApiKey": "Sanal API Anahtarını Sil",
|
||||||
|
"actionGetVirtualApiKey": "Sanal API Anahtarı Al",
|
||||||
|
"actionListVirtualApiKeys": "Sanal API Anahtarlarını Listele",
|
||||||
|
"actionUpdateVirtualApiKey": "Sanal API Anahtarı Güncelle",
|
||||||
"actionApplyBlueprint": "Planı Uygula",
|
"actionApplyBlueprint": "Planı Uygula",
|
||||||
"actionListBlueprints": "Plan Listesini Görüntüle",
|
"actionListBlueprints": "Plan Listesini Görüntüle",
|
||||||
"actionGetBlueprint": "Planı Elde Et",
|
"actionGetBlueprint": "Planı Elde Et",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "Kaynak PIN Kodunu Ayarla",
|
"actionSetResourcePincode": "Kaynak PIN Kodunu Ayarla",
|
||||||
"actionSetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesi Ayarla",
|
"actionSetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesi Ayarla",
|
||||||
"actionGetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesini Al",
|
"actionGetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesini Al",
|
||||||
|
"actionListResourcePolicies": "Kaynak Politikalarını Listele",
|
||||||
|
"actionCreateResourcePolicy": "Kaynak Politikası Oluştur",
|
||||||
"actionGetResourcePolicy": "Kaynak Politikasını Al",
|
"actionGetResourcePolicy": "Kaynak Politikasını Al",
|
||||||
"actionUpdateResourcePolicy": "Kaynak Politikasını Güncelle",
|
"actionUpdateResourcePolicy": "Kaynak Politikasını Güncelle",
|
||||||
|
"actionDeleteResourcePolicy": "Kaynak Politikasını Sil",
|
||||||
"actionSetResourcePolicyUsers": "Kaynak Politika Kullanıcılarını Ayarla",
|
"actionSetResourcePolicyUsers": "Kaynak Politika Kullanıcılarını Ayarla",
|
||||||
"actionSetResourcePolicyRoles": "Kaynak Politika Rolleri Ayarla",
|
"actionSetResourcePolicyRoles": "Kaynak Politika Rolleri Ayarla",
|
||||||
"actionSetResourcePolicyPassword": "Kaynak Politika Şifresini Ayarla",
|
"actionSetResourcePolicyPassword": "Kaynak Politika Şifresini Ayarla",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "Davetiye",
|
"sidebarInvitations": "Davetiye",
|
||||||
"sidebarRoles": "Roller",
|
"sidebarRoles": "Roller",
|
||||||
"sidebarShareableLinks": "Paylaşılabilir Bağlantılar",
|
"sidebarShareableLinks": "Paylaşılabilir Bağlantılar",
|
||||||
|
"sidebarAiGateway": "AI Geçidi",
|
||||||
|
"sidebarAiProviders": "Sağlayıcılar",
|
||||||
|
"commandAiProviders": "AI Sağlayıcıları",
|
||||||
|
"sidebarVirtualApiKeys": "Sanal API Anahtarları",
|
||||||
|
"sidebarMyApiKeys": "API Anahtarlarınız",
|
||||||
|
"sidebarAccount": "Başlatıcı",
|
||||||
|
"commandVirtualApiKeys": "Sanal API Anahtarları",
|
||||||
|
"virtualApiKeysTitle": "Sanal API Anahtarlarını Yönet",
|
||||||
|
"virtualApiKeysDescription": "AI Geçitlerine erişim için manuel API anahtarları oluşturun ve yönetin",
|
||||||
|
"virtualApiKeysTabIdentity": "Kimlik Anahtarları",
|
||||||
|
"virtualApiKeysTabVirtual": "Sanal Anahtarlar",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "Her Kullanıcı İçin Kimlik Anahtarları",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "Her kullanıcının bu organizasyon için zaten bir Pangolin kimlik anahtarı var. Hesaplarına özgüdür ve erişebilecekleri AI geçitlerine kimlik doğrulama sağlar.",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "Örnek",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "Kullanıcıların Anahtarlarını Nasıl Aldığı",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "Tarayıcıda herkese açık bir AI geçit URL'sini ziyaret edin ve hesaplarıyla giriş yapın.",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "Veya giriş yaparken <url></url> adresine gidin.",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "Sanal Anahtarlar sekmesinde ek sanal API anahtarları oluşturabilirsiniz. Bu anahtarlar belirli herkese açık AI geçitlerine göre kapsamlandırılabilir ve isteğe bağlı olarak bir kullanıcıyla ilişkilendirilebilir. Bir anahtar oluşturmak seçilen kamu AI geçitlerine erişim sağlar.",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "Anahtarı Manuel Olarak Oluştur",
|
||||||
|
"virtualApiKeysEmailIdentity": "Kimlik Anahtarlarını E-posta Gönder",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "Seçilen her kullanıcıya veya role Pangolin kimlik anahtarlarını gönderin.",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "Tüm kullanıcılara gönder",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "Bir hesap e-posta adresi olan her organizasyon üyesine e-posta gönderin.",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "Kullanıcılar",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "Roller",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "E-postaları Gönder",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "Kimlik anahtarları e-posta ile gönderildi",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "{sent} e-posta gönderildi.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "E-posta adresine sahip olmayan {skipped} kullanıcılar atlandı.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "En az bir kullanıcı veya rol seçin ya da tüm kullanıcılara gönderin.",
|
||||||
|
"virtualApiKeysEmailIdentityError": "Kimlik anahtarlarını gönderme hatası",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "Kimlik anahtarları e-posta ile gönderilemiyor",
|
||||||
|
"virtualApiKeysBannerTitle": "Her Kullanıcı İçin Kimlik Anahtarları",
|
||||||
|
"virtualApiKeysBannerDescription": "Her kullanıcı {keysUrl}'de kullanılabilir bir kimlik anahtarına sahiptir. Burada genel AI geçitlerine doğrudan erişim sağlayan anahtarlar da oluşturabilirsiniz.",
|
||||||
|
"virtualApiKeysBannerButtonText": "Kimlik Anahtarlarını Görüntüle",
|
||||||
|
"virtualApiKeys": "Sanal API Anahtarları",
|
||||||
|
"virtualApiKeysSearch": "Anahtarları ara...",
|
||||||
|
"virtualApiKeysCreate": "Sanal API Anahtarı Oluştur",
|
||||||
|
"virtualApiKeysCreateDescription": "Bu organizasyonda genel AI geçitlerini arayabilecek manuel bir anahtar mint edin",
|
||||||
|
"virtualApiKeysCreateButton": "Anahtar Oluştur",
|
||||||
|
"virtualApiKeysEmpty": "Henüz sanal API anahtarı yok",
|
||||||
|
"virtualApiKeysName": "Ad",
|
||||||
|
"virtualApiKeysDescriptionOptional": "Açıklama (isteğe bağlı)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "Kullanıcıyla İlişkilendir (isteğe bağlı)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "Bu anahtarı bir kullanıcıyla ilişkilendirerek kullanım takibini yapın. Oluşturulduktan hemen sonra bu anahtar seçilen kamu AI geçitlerine erişim sağlar. Anahtar, kullanıcının profilinde de gösterilebilir.",
|
||||||
|
"virtualApiKeysAllResources": "Tüm kamu AI geçitleri",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "Bu anahtara organizasyondaki tüm genel AI geçitlerine erişim izni ver",
|
||||||
|
"virtualApiKeysSelectResources": "Genel AI Geçitleri",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "Kaynakları seçin",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "Bu anahtarın hangi kamu AI geçitlerine erişebileceğini seçin",
|
||||||
|
"virtualApiKeysNoResources": "Kaynak yok",
|
||||||
|
"virtualApiKeysSecret": "Anahtar",
|
||||||
|
"virtualApiKeysCopyKey": "Bu anahtarı kopyalayın. Tablo veya düzenlemede tekrar görebilirsiniz.",
|
||||||
|
"virtualApiKeysViewSecret": "Gizliyi Görüntüle",
|
||||||
|
"virtualApiKeysViewSecretTitle": "Sanal API Anahtarı Gizli",
|
||||||
|
"virtualApiKeysViewSecretDescription": "Bu gizli, bu anahtara atanmış kamu AI geçitlerine erişim sağlar",
|
||||||
|
"virtualApiKeysEdit": "Sanal API Anahtarı Düzenle",
|
||||||
|
"virtualApiKeysEditDescription": "Bu anahtar için ilişkili kullanıcı ve kamu AI geçidine erişimi güncelle",
|
||||||
|
"virtualApiKeysSaveButton": "Değişiklikleri Kaydet",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "En az bir kamu AI geçidi seçin veya tüm kamu AI geçitlerini etkinleştirin",
|
||||||
|
"virtualApiKeysUpdated": "Sanal API anahtarı güncellendi",
|
||||||
|
"virtualApiKeysUpdatedDescription": "Sanal API anahtarı güncellendi",
|
||||||
|
"virtualApiKeysErrorUpdate": "Sanal API anahtarı güncellenirken hata",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "Sanal API anahtarı güncellenemedi",
|
||||||
|
"virtualApiKeysErrorCreate": "Sanal API anahtarı oluşturulurken hata",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "Sanal API anahtarı oluşturulamadı",
|
||||||
|
"virtualApiKeysErrorDelete": "Sanal API anahtarı silinirken hata",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "Sanal API anahtarı silinemedi",
|
||||||
|
"virtualApiKeysDeleted": "Sanal API anahtarı silindi",
|
||||||
|
"virtualApiKeysDeletedDescription": "Sanal API anahtarı silinmiştir",
|
||||||
|
"virtualApiKeysDelete": "Sanal API Anahtarını Sil",
|
||||||
|
"virtualApiKeysDeleteConfirm": "Anahtarı Sil",
|
||||||
|
"virtualApiKeysQuestionRemove": "Bu sanal API anahtarını silmek istediğinizden emin misiniz?",
|
||||||
|
"virtualApiKeysMessageRemove": "Bu anahtarı kullanan müşteriler hemen erişimlerini kaybedecektir.",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "Gizli yüklenirken hata",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "Sanal API anahtarı gizli yüklenemedi",
|
||||||
|
"virtualApiKeysFilterUnassigned": "Atanmamış",
|
||||||
|
"virtualApiKeysInferenceBudget": "Bütçe",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "Bu anahtarın AI kullanımını harcama veya jeton sınırlarına göre nasıl kısıtladığını yapılandırın",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "Anahtar oluşturulduğunda e-posta gönder",
|
||||||
|
"virtualApiKeysEmailThisKey": "Bu anahtarı e-posta gönder",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "Anahtar oluşturulduktan sonra ilişkili kullanıcıya ve ek adreslere gönderin",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "Geçerli anahtarı ilişkili kullanıcıya ve ek adreslere gönderin",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "Bu sunucuda e-posta yapılandırılmamış",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "SMTP yapılandırmasını yaparak sanal API anahtarlarını e-posta gönderin.",
|
||||||
|
"virtualApiKeysEmailSendToUser": "İlişkili kullanıcıya gönder",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "Anahtarı ilişkili kullanıcının hesabını alın ve e-posta gönderin",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "İlişkili bir kullanıcı seçin ve o kullanıcıya anahtarı gönder",
|
||||||
|
"virtualApiKeysEmailAdditional": "Ek e-postalar",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "E-posta ekle ve Enter tuşuna basın",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "İlişkili kullanıcının hesabını seçin veya en az bir e-posta adresi ekleyin",
|
||||||
|
"myVirtualApiKeysTitle": "API Anahtarlarınız",
|
||||||
|
"myVirtualApiKeysDescription": "Bu organizasyonda size atanmış kimlik anahtarınızı ve sanal API anahtarlarınızı görüntüleyin",
|
||||||
|
"myVirtualApiKeysResourceTitle": "{resourceName} için API Anahtarlarınız",
|
||||||
|
"myVirtualApiKeysResourceDescription": "{resourceName} erişebileceğiniz kimlik anahtarınızı ve size atanmış sanal API anahtarlarını görüntüleyin",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "Kimlik Anahtarı",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "Kişisel API Anahtarınız",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "Bu organizasyon için kişisel anahtarınız. Hesabınıza özeldir ve AI Gateway kaynaklarına çağrı yaptığınızda sizi tanımlar.",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "{resourceName} için Kişisel API Anahtarınız",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "Bu organizasyon için kişisel anahtarınız. {resourceName} 'e çağrı yaparken kullanın.",
|
||||||
|
"myVirtualApiKeysManualTitle": "Atanmış Anahtarlar",
|
||||||
|
"myVirtualApiKeysManualDescription": "Hesabınıza yönetici tarafından atanmış manuel sanal API anahtarları",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "{resourceName} erişebilecek hesabınıza atanmış manuel sanal API anahtarları",
|
||||||
|
"myVirtualApiKeysManualEmpty": "Henüz atanmış anahtar bulunmamaktadır",
|
||||||
|
"myVirtualApiKeysKindUser": "Kimlik",
|
||||||
|
"myVirtualApiKeysKindManual": "Manuel",
|
||||||
|
"myVirtualApiKeysUnnamed": "İsimsiz anahtar",
|
||||||
|
"myVirtualApiKeysRevealSecret": "Gizliyi Açıklayın",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "Bu gizli, sizi AI Gateway kaynaklarına kimlik doğrular",
|
||||||
|
"aiProvidersTitle": "AI Sağlayıcıları",
|
||||||
|
"aiProvidersDescription": "Bu organizasyonda AI iş yükleri için model sağlayıcılarla bağlantı kurun",
|
||||||
|
"aiProvidersBannerTitle": "Model Sağlayıcılarla Bağlan",
|
||||||
|
"aiProvidersBannerDescription": "Sağlayıcılar, Pangolin'in AI iş yükleri için kullandığı model arka uçlarıdır. Burada OpenAI, Anthropic ve diğer sağlayıcıları bağlayın, ardından bu sağlayıcıları genel AI geçitlerine ekleyin, böylece kullanıcılar modelleri kimlik farkındalığı ile erişebilir.",
|
||||||
|
"aiProvidersAdd": "Sağlayıcı Ekle",
|
||||||
|
"aiProvidersSearch": "Sağlayıcıları ara...",
|
||||||
|
"aiProvidersEmpty": "Henüz AI sağlayıcısı yok",
|
||||||
|
"aiProviderCreate": "AI Sağlayıcı Oluştur",
|
||||||
|
"aiProviderCreateDescription": "Bu organizasyon için bir model sağlayıcı ekleyin",
|
||||||
|
"aiProviderSeeAll": "Tüm Sağlayıcıları Gör",
|
||||||
|
"aiProviderSetting": "{providerName} için Sağlayıcı Ayarları",
|
||||||
|
"aiProviderSettingDescription": "Bu AI sağlayıcısını yapılandırın",
|
||||||
|
"aiProviderGeneral": "Genel",
|
||||||
|
"aiProviderGeneralDescription": "Bu sağlayıcı için temel ayarlar",
|
||||||
|
"aiProviderConfiguration": "Yapılandırma",
|
||||||
|
"aiProviderConfigurationDescription": "Bu sağlayıcının ağ yönlendirme ve kimlik doğrulaması",
|
||||||
|
"aiProviderNetworkSettings": "Ağ Ayarları",
|
||||||
|
"aiProviderNetworkSettingsDescription": "Trafiğin bu sağlayıcıya nasıl ulaşacağını seçin",
|
||||||
|
"aiProviderAuthSettings": "Kimlik Doğrulama",
|
||||||
|
"aiProviderAuthSettingsDescription": "Bu sağlayıcının URL'sine gelen isteklerde kimlik doğrulamalarını yapılandırın",
|
||||||
|
"aiProviderBudgetSettings": "Bütçe",
|
||||||
|
"aiProviderBudgetSettingsDescription": "Bu sağlayıcının harcama veya jeton sınırlamalarına dayalı olarak kullanımını nasıl kısıtlayacağını yapılandırın",
|
||||||
|
"aiBudgetAdd": "Bütçe Ekle",
|
||||||
|
"aiBudgetEmpty": "Henüz bütçe ayarlanmamış. Harcama veya jeton sınırını ayarlamak için Bütçe Ekle'ye tıklayın.",
|
||||||
|
"aiBudgetUnit": "Harcama Türü",
|
||||||
|
"aiBudgetPeriod": "Sıfırlama Dönemi",
|
||||||
|
"aiBudgetAmount": "Maksimum Harcama",
|
||||||
|
"aiBudgetAmountPlaceholder": "Maksimum harcama",
|
||||||
|
"aiBudgetPeriodHourly": "Saatlik",
|
||||||
|
"aiBudgetPeriodDaily": "Günlük",
|
||||||
|
"aiBudgetPeriodWeekly": "Haftalık",
|
||||||
|
"aiBudgetPeriodMonthly": "Aylık",
|
||||||
|
"aiBudgetPeriodYearly": "Yıllık",
|
||||||
|
"aiBudgetPeriodLifetime": "Ömür Boyu",
|
||||||
|
"aiBudgetUnitUsd": "USD",
|
||||||
|
"aiBudgetUnitTokens": "Jetonlar",
|
||||||
|
"aiBudgetConflictError": "Bu sıfırlama dönemi ve harcama türü için zaten bir bütçe var",
|
||||||
|
"aiBudgetInvalidAmountError": "0'dan büyük bir maksimum harcama girin",
|
||||||
|
"aiBudgetUpdated": "Bütçeler Güncellendi",
|
||||||
|
"aiBudgetErrorSave": "Bütçeleri güncelleme başarısız",
|
||||||
|
"aiProviderType": "Sağlayıcı Türü",
|
||||||
|
"aiProviderTypeSearch": "Sağlayıcıları ara...",
|
||||||
|
"aiProviderTypeNotFound": "Hiçbir sağlayıcı türü bulunamadı",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Geçidi",
|
||||||
|
"aiProviderTypeCustom": "Özel",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Geçidi API",
|
||||||
|
"aiProviderTypeCustomDescription": "Kendi uç noktanızı getirin veya site hedefleri üzerinden yönlendirin",
|
||||||
|
"aiProviderUpstreamUrl": "Yukarı Akış URL'si",
|
||||||
|
"aiProviderUpstreamUrlDescription": "Sağlayıcı API için Temel URL",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "Bu sağlayıcı için varsayılan yukarı akış URL'sini kullanmak için boş bırakın",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "Etkili Yukarı Akış URL'si",
|
||||||
|
"aiProviderApiKey": "API Anahtarı",
|
||||||
|
"aiProviderApiKeyDescription": "Bu sağlayıcıya yapılan istekleri kimlik doğrulamak için kullanılan API anahtarı",
|
||||||
|
"aiProviderCustomHeadersDescription": "Bu sağlayıcıya yapılan her istekte gönderilen başlıklar. Yeni satır ile ayrılmış: Başlık-Adı: değer",
|
||||||
|
"aiProviderApiKeyLastChars": "API Anahtarı",
|
||||||
|
"aiProviderAuthType": "Kimlik Doğrulama Türü",
|
||||||
|
"aiProviderAuthTypeSearch": "Kimlik doğrulama türlerini ara...",
|
||||||
|
"aiProviderAuthTypeNotFound": "Kimlik doğrulama türü bulunamadı",
|
||||||
|
"aiProviderAuthTypeBearer": "Taşıyıcı",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "Yetkilendirme: Bearer anahtarı. OpenAI ve çoğu sağlayıcı tarafından kullanılır",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key başlığı. Anthropic tarafından kullanılır",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key başlığı. Google Gemini tarafından kullanılır",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "Yetkilendirme: Splunk anahtarı. Splunk HTTP Olay Toplayıcı tarafından kullanılır",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Geçidi",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-yetkilendirme: Bearer anahtarı. Cloudflare AI Gateway tarafından kullanılır",
|
||||||
|
"aiProviderAuthTypeNone": "Kimlik Doğrulama Yok",
|
||||||
|
"aiProviderAuthTypePassthrough": "Geçir",
|
||||||
|
"aiProviderAuthTypeDescription": "Yukarıdaki API, istekleri nasıl kimlik doğrular",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "Arayanın API anahtarı başlıklarını yukarı akışa iletin",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "Yukarı akışa kimlik doğrulama başlıkları göndermeyin",
|
||||||
|
"aiProviderRoutingMode": "Yönlendirme Modu",
|
||||||
|
"aiProviderRoutingModeDescription": "Trafiği bir yukarı akış URL'sine veya sitenizdeki HTTP hedeflerine gönderin",
|
||||||
|
"aiProviderRoutingModeUrl": "Yukarı Akış URL'si",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "Bir kamuya açık ya da özel API temel URL'si çağırın",
|
||||||
|
"aiProviderRoutingModeTarget": "Site Hedefleri",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "Siteniz üzerindeki hedefler üzerinden yönlendirin",
|
||||||
|
"aiProviderRoutingModeTargetNote": "Bu sağlayıcıyı oluşturduktan sonra, site hedeflerini Ağ Ayarları sekmesinde yapılandırın.",
|
||||||
|
"aiProviderTargetNoOne": "Bu sağlayıcının herhangi bir hedefi yok. Sitemiz üzerinden istekleri yönlendirmek için bir hedef ekleyin.",
|
||||||
|
"aiProviderSkipTlsVerification": "TLS Doğrulamayı Atla",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "Yukarı akış bağlantısı için TLS sertifika doğrulamasını devre dışı bırakın",
|
||||||
|
"aiProviderBudget": "Bütçe",
|
||||||
|
"aiProviderBudgetDescription": "Bu sağlayıcı için isteğe bağlı harcama veya jeton bütçesi",
|
||||||
|
"aiProviderBudgetAmount": "Bütçe Miktarı",
|
||||||
|
"aiProviderBudgetUnit": "Bütçe Birimi",
|
||||||
|
"aiProviderBudgetUnitUsd": "USD",
|
||||||
|
"aiProviderBudgetUnitTokens": "Jetonlar",
|
||||||
|
"aiProviderEnabled": "Etkin",
|
||||||
|
"aiProviderEnabledDescription": "Bu sağlayıcıyı tüm kaynaklarda tamamen devre dışı bırakın",
|
||||||
|
"aiProviderErrorCreate": "AI sağlayıcı oluşturma başarısız",
|
||||||
|
"aiProviderErrorUpdate": "AI sağlayıcı güncelleme başarısız",
|
||||||
|
"aiProviderErrorDelete": "AI sağlayıcı silme başarısız",
|
||||||
|
"aiProviderErrorLoad": "AI sağlayıcı yükleme başarısız",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "Geçerli bir yukarı akış URL'si girin",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "Bu sağlayıcı için yukarı akış URL'si gereklidir",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "Kimlik doğrulama türü zorunludur",
|
||||||
|
"aiProviderErrorApiKeyRequired": "API anahtarı zorunludur",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "Site hedefleri yönlendirme sadece özel sağlayıcılar için geçerlidir",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "En az bir API yetenek seçin",
|
||||||
|
"aiProviderCapabilities": "API Yetenekleri",
|
||||||
|
"aiProviderCapabilitiesDescription": "Bu sağlayıcının hangi API formatlarını kullanabileceğini seçin. Bilinen sağlayıcılar, önerilen varsayılanlarla başlar.",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "Bu özel sağlayıcının hangi API formatlarını işleyebileceğini seçin",
|
||||||
|
"aiProviderCapabilitiesSelect": "Yetenekleri seçin",
|
||||||
|
"aiProviderCapabilitiesEmpty": "Hiçbir yetenek bulunamadı",
|
||||||
|
"aiProviderCapabilitiesSearch": "Yetenekleri ara...",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI Sohbet Tamamlama",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "/v1/chat/tamamlama desteği sağlar",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI Yanıtları",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "/v1/yanıtlar desteği sağlar",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic Mesajlar",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "/v1/mesajlar desteği sağlar",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini İçerik Üret",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "Doğrudan Gemini API desteği sağlar",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Bedrock Modeli Çağır",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "Amazon Bedrock InvokeModel desteği sağlar",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex İçerik Üret",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "Vertex AI Gemini formatı desteği sağlar",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex Ham Tahmin",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "Vertex AI HamTahmin desteği sağlar",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock Sohbet",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "Amazon Bedrock Sohbet API desteği sağlar",
|
||||||
|
"aiProviderCreated": "AI sağlayıcı yaratıldı",
|
||||||
|
"aiProviderUpdated": "AI sağlayıcı güncellendi",
|
||||||
|
"aiProviderDeleted": "AI sağlayıcı silindi",
|
||||||
|
"aiProviderDelete": "Sağlayıcıyı Sil",
|
||||||
|
"aiProviderDeleteConfirm": "Sağlayıcıyı Sil",
|
||||||
|
"aiProviderQuestionRemove": "Bu AI sağlayıcıyı silmek istediğinizden emin misiniz?",
|
||||||
|
"aiProviderMessageRemove": "Bu, sağlayıcıyı ve modellerini ve hedeflerini kalıcı olarak silecektir. Geri alınamaz.",
|
||||||
|
"aiProviderErrorNoUpdate": "AI sağlayıcı güncellenemiyor",
|
||||||
|
"aiProviderModels": "Modeller",
|
||||||
|
"aiProviderModelsDescription": "Bu sağlayıcı için izin ve engelleme listelerini tanımlayın. İstekler bir izin girişini karşılamalı ve bir engelleme girişini karşılamamalıdır.",
|
||||||
|
"aiProviderCreateModelsDescription": "Bu sağlayıcının hizmet verebileceği modelleri seçin. Boş bir izin listesi tüm trafiği reddeder. Modelleri daha sonra ekleyebilir veya değiştirebilirsiniz.",
|
||||||
|
"aiProviderModelsPlaceholder": "Modelleri arayın veya özel bir anahtar yazın",
|
||||||
|
"aiProviderModelsAllow": "İzin Listesi",
|
||||||
|
"aiProviderModelsAllowDescription": "Bu sağlayıcı aracılığıyla kullanılabilecek modeller. Boş, tümünü reddet anlamına gelir.",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "Model anahtarını girin",
|
||||||
|
"aiProviderModelsAllowEmpty": "Henüz model eklenmedi.",
|
||||||
|
"aiProviderModelsBlock": "Engelleme Listesi",
|
||||||
|
"aiProviderModelsBlockDescription": "Bir izin girdisiyle eşleşseler bile reddedilecek modeller.",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "Model anahtarını girin",
|
||||||
|
"aiProviderModelsBlockEmpty": "Henüz model eklenmedi.",
|
||||||
|
"aiProviderModelsAdd": "Modelleri Ekle",
|
||||||
|
"aiProviderModelsClearAll": "Hepsini Temizle",
|
||||||
|
"aiProviderModelsAddCustom": "\"{key}\" ekle",
|
||||||
|
"aiProviderModelsAddCustomHint": "Bu özel model anahtarını eklemek için Enter'a basın.",
|
||||||
|
"aiProviderModelsAddBulk": "{count} özel anahtar ekle",
|
||||||
|
"aiProviderModelsAddBulkHint": "{count} özel anahtarı eklemek için Enter'a basın.",
|
||||||
|
"aiProviderModelsAddOne": "Şimdi {key} ekle",
|
||||||
|
"aiProviderModelsAddSelected": "Seçileni Ekle",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} seçildi",
|
||||||
|
"aiProviderModelsSelectAll": "Hepsini seç",
|
||||||
|
"aiProviderModelsClearSelected": "Temizle",
|
||||||
|
"aiProviderModelsBulkHint": "Bilinen modelleri seçin veya özel bir anahtar yazın.",
|
||||||
|
"aiProviderModelsCatalogEmpty": "Eşleşen katalog modeli yok.",
|
||||||
|
"aiProviderModelsCatalogHeading": "Bilinen Modeller",
|
||||||
|
"aiProviderModelsAllLabel": "Tüm modeller",
|
||||||
|
"aiProviderModelsAllPatternHint": "Genel arama deseni: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "Tüm modellere izin ver",
|
||||||
|
"aiProviderModelsAddAllBlock": "Tüm modelleri engelle",
|
||||||
|
"aiProviderModelsAddAllDescription": "* arama desenini kullanarak her model anahtarı eşleşir.",
|
||||||
|
"aiProviderModelsViewMore": "Daha fazla göster ({count})",
|
||||||
|
"aiProviderModelsViewLess": "Daha az göster",
|
||||||
|
"aiProviderModelsRemove": "Modeli kaldır",
|
||||||
|
"aiProviderModelsEditHint": "Model ayarlarını düzenlemek için tıklayın",
|
||||||
|
"aiProviderModelsSourceCatalog": "Bilinen katalog modeli",
|
||||||
|
"aiProviderModelsSourceCustom": "Özel model anahtarı",
|
||||||
|
"aiProviderModelsSourcePattern": "Genel arama deseni",
|
||||||
|
"aiProviderModelsSourceAll": "Her model anahtarı ile eşleşir",
|
||||||
|
"aiProviderModelsBudgetConfigured": "Bütçe yapılandırılmış",
|
||||||
|
"aiProviderModelsEditTitle": "Modeli düzenle",
|
||||||
|
"aiProviderModelsEditDescription": "Model anahtarını güncelleyin veya bütçesini yapılandırın.",
|
||||||
|
"aiProviderModelsBudgetTab": "Bütçe",
|
||||||
|
"aiProviderModelsKeyLabel": "Model Anahtarı",
|
||||||
|
"aiProviderModelsKeyRequired": "Bir model anahtarı girin",
|
||||||
|
"aiProviderModelsKeyDuplicate": "Bu model anahtarı zaten bir listede var",
|
||||||
|
"aiProviderModelsOverlapError": "Bu desenler her iki listede de olamaz: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "Modeller güncellendi",
|
||||||
|
"aiProviderModelsErrorUpdate": "Modeller güncellenemedi",
|
||||||
|
"aiResourceProviders": "Sağlayıcılar",
|
||||||
|
"aiResourceProvidersDescription": "Bu AI ağ geçidi kullanabileceği AI sağlayıcılarını seçin",
|
||||||
|
"aiResourceProvidersHelp": "Sağlayıcıları ekle ve miras almayı (her sağlayıcının listesini kullanmayı) ya da seçmeyi (bu kaynak için bir izin listesini seç) seçin. Birbirine zıt sağlayıcıların izin desenleri izin verilmez.",
|
||||||
|
"aiResourceProvidersSelect": "Sağlayıcıları seçin",
|
||||||
|
"aiResourceProvidersEmpty": "Hiçbir AI sağlayıcı bulunamadı",
|
||||||
|
"aiResourceProvidersNoneAttached": "Henüz hiç sağlayıcı eklenmedi.",
|
||||||
|
"aiResourceProvidersAdd": "Sağlayıcı ekle",
|
||||||
|
"aiResourceProvidersRemove": "Sağlayıcıyı kaldır",
|
||||||
|
"aiResourceProviderToggleEnabled": "Bu kaynaktaki sağlayıcıyı etkinleştir veya devre dışı bırak",
|
||||||
|
"aiResourceProviderDisabled": "Devre Dışı",
|
||||||
|
"aiResourceProvidersUpdated": "Sağlayıcılar güncellendi",
|
||||||
|
"aiResourceProvidersErrorUpdate": "Sağlayıcılar güncellenemedi",
|
||||||
|
"aiResourceProviderEditDescription": "Bu sağlayıcının modellerinin bu kaynakta nasıl göstereceğini seçin.",
|
||||||
|
"viewProviderSettings": "Sağlayıcı Ayarlarını Görüntüle",
|
||||||
|
"aiResourceProviderMode": "Erişim modu",
|
||||||
|
"aiResourceProviderModeInherit": "Miras Al",
|
||||||
|
"aiResourceProviderModeSelect": "Seç",
|
||||||
|
"aiResourceProviderModeSelectSummary": "Seç · {count} modeller",
|
||||||
|
"aiResourceProviderModeInheritHelp": "Bu sağlayıcıda yapılandırıldığı gibi izin ve engelleme listelerini kullanın.",
|
||||||
|
"aiResourceProviderModeSelectHelp": "Bu kaynak için bu sağlayıcının izin listesi modellerinin bir alt kümesini seçin.",
|
||||||
|
"aiResourceProviderAllowModels": "İzin listesi",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "Modelleri seçin",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "Modelleri ara...",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "Hiçbir model bulunamadı",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "Sadece sağlayıcının izin listesindeki modeller seçilebilir.",
|
||||||
|
"aiResourceAliasRequired": "AI ağ geçitleri için takma ad gereklidir",
|
||||||
|
"aiResourceDomainConfiguration": "Alan Adı Yapılandırması",
|
||||||
|
"aiResourceDomainConfigurationDescription": "Bu AI ağ geçidine ulaşmak için müşterilerin kullanacağı alan adını seçin.",
|
||||||
|
"aiUsageAnalyticsTitle": "AI Kullanım Analizi",
|
||||||
|
"aiUsageAnalyticsDescription": "AI ağ geçidi maliyetini, token kullanımını ve sağlayıcılar, kaynaklar, roller ve kullanıcılar genelindeki aktiviteyi analiz edin",
|
||||||
|
"aiUsageTabOverview": "Genel Bakış",
|
||||||
|
"aiUsageTabProviders": "Sağlayıcılar",
|
||||||
|
"aiUsageTabResources": "Kaynaklar",
|
||||||
|
"aiUsageFilterProvider": "Sağlayıcı",
|
||||||
|
"aiUsageFilterModel": "Model",
|
||||||
|
"aiUsageFilterResource": "Kaynak",
|
||||||
|
"aiUsageFilterRole": "Rol",
|
||||||
|
"aiUsageFilterUser": "Kullanıcı",
|
||||||
|
"aiUsageFilterAllProviders": "Tüm Sağlayıcılar",
|
||||||
|
"aiUsageFilterAllModels": "Tüm Modeller",
|
||||||
|
"aiUsageFilterAllResources": "Tüm Kaynaklar",
|
||||||
|
"aiUsageFilterAllRoles": "Tüm Roller",
|
||||||
|
"aiUsageFilterAllUsers": "Tüm Kullanıcılar",
|
||||||
|
"aiUsageFilterSearch": "Ara...",
|
||||||
|
"aiUsageFilterNotFound": "Hiçbir seçenek bulunamadı",
|
||||||
|
"aiUsageResetFilters": "Filtreleri Sıfırla",
|
||||||
|
"aiUsageRefresh": "Yenile",
|
||||||
|
"aiUsageTokenTypePrompt": "Yazım",
|
||||||
|
"aiUsageTokenTypeCacheRead": "Önbellek okuma",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "Önbellek yazma",
|
||||||
|
"aiUsageTokenTypeCompletion": "Tamamlama",
|
||||||
|
"aiUsageTokenTypeReasoning": "Muhakeme",
|
||||||
|
"aiUsageRequests": "İstekler",
|
||||||
|
"aiUsageCost": "Maliyet",
|
||||||
|
"aiUsageTokens": "Jetonlar",
|
||||||
|
"aiUsageOther": "Diğer",
|
||||||
|
"aiUsageTotalRequests": "Toplam İstekler",
|
||||||
|
"aiUsageTotalTokens": "Toplam Jetonlar",
|
||||||
|
"aiUsageTotalCost": "Toplam Maliyet",
|
||||||
|
"aiUsageEstimated": "Tahmini",
|
||||||
|
"aiUsageRequestVolume": "İstek Hacmi",
|
||||||
|
"aiUsageTokenUsage": "Jeton Kullanımı",
|
||||||
|
"aiUsageModelCost": "Model Maliyeti",
|
||||||
|
"aiUsageModelTokens": "Model Jetonları",
|
||||||
|
"aiUsageTopModels": "En İyi Modeller",
|
||||||
|
"aiUsageTopProviders": "En İyi Sağlayıcılar",
|
||||||
|
"aiUsageProviderCost": "Sağlayıcı Maliyeti",
|
||||||
|
"aiUsageProviderTokenUsage": "Sağlayıcı Jeton Kullanımı",
|
||||||
|
"aiUsageTopResources": "En İyi Kaynaklar",
|
||||||
|
"aiUsageResourceCost": "Kaynak Maliyeti",
|
||||||
|
"aiUsageResourceTokenUsage": "Kaynak Jeton Kullanımı",
|
||||||
|
"aiUsageResourceTypePublic": "Kamusal Kaynak",
|
||||||
|
"aiUsageResourceTypeSite": "Özel Kaynak",
|
||||||
|
"aiUsageNoResource": "Kaynak yok",
|
||||||
|
"aiUsageRolesTab": "Roller",
|
||||||
|
"aiUsageUsersTab": "Kullanıcılar",
|
||||||
|
"aiUsageTopRoles": "En İyi Roller",
|
||||||
|
"aiUsageRoleCost": "Rol Maliyeti",
|
||||||
|
"aiUsageRoleTokenUsage": "Rol Jeton Kullanımı",
|
||||||
|
"aiUsageTopUsers": "En İyi Kullanıcılar",
|
||||||
|
"aiUsageUserCost": "Kullanıcı Maliyeti",
|
||||||
|
"aiUsageUserTokenUsage": "Kullanıcı Jeton Kullanımı",
|
||||||
|
"aiUsageUnknownUser": "Bilinmeyen kullanıcı",
|
||||||
|
"aiUsageVirtualApiKeysTab": "Sanal API Anahtarları",
|
||||||
|
"aiUsageFilterVirtualApiKey": "Sanal API Anahtarı",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "Tüm Sanal API Anahtarları",
|
||||||
|
"aiUsageTopVirtualApiKeys": "En İyi Sanal API Anahtarları",
|
||||||
|
"aiUsageVirtualApiKeyCost": "Sanal API Anahtar Maliyeti",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "Sanal API Anahtar Jeton Kullanımı",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "Sanal API anahtarı yok",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "İsimsiz anahtar",
|
||||||
|
"aiUsageLoading": "Yükleniyor...",
|
||||||
|
"aiUsageNoData": "Veri yok",
|
||||||
|
"resourceBudgetSettings": "Bütçe",
|
||||||
|
"resourceBudgetSettingsDescription": "Bu AI ağ geçidi harcama veya jeton limitlerine göre kullanım nasıl sınırlayacağını yapılandırın",
|
||||||
"sidebarApiKeys": "API Anahtarları",
|
"sidebarApiKeys": "API Anahtarları",
|
||||||
"sidebarProvisioning": "Tedarik",
|
"sidebarProvisioning": "Tedarik",
|
||||||
"sidebarSettings": "Ayarlar",
|
"sidebarSettings": "Ayarlar",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "Alt ağ",
|
"subnetPlaceholder": "Alt ağ",
|
||||||
"addressDescription": "İstemcinin dahili adresi. Organizasyon alt ağı içinde olmalıdır.",
|
"addressDescription": "İstemcinin dahili adresi. Organizasyon alt ağı içinde olmalıdır.",
|
||||||
"selectSites": "Siteleri seçin",
|
"selectSites": "Siteleri seçin",
|
||||||
|
"selectResources": "Kaynakları seçin",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, one {# kaynak} other {# kaynaklar}}",
|
||||||
"selectLabels": "Etiketleri seçin",
|
"selectLabels": "Etiketleri seçin",
|
||||||
"sitesDescription": "Müşteri seçilen sitelere bağlantı kuracaktır",
|
"sitesDescription": "Müşteri seçilen sitelere bağlantı kuracaktır",
|
||||||
"clientInstallOlm": "Olm Yükle",
|
"clientInstallOlm": "Olm Yükle",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "Zaman saniye cinsindendir",
|
"timeIsInSeconds": "Zaman saniye cinsindendir",
|
||||||
"requireDeviceApproval": "Cihaz Onaylarını Gerektir",
|
"requireDeviceApproval": "Cihaz Onaylarını Gerektir",
|
||||||
"requireDeviceApprovalDescription": "Bu role sahip kullanıcıların yeni cihazlarının bağlanabilmesi ve kaynaklara erişebilmesi için bir yönetici tarafından onaylanması gerekiyor.",
|
"requireDeviceApprovalDescription": "Bu role sahip kullanıcıların yeni cihazlarının bağlanabilmesi ve kaynaklara erişebilmesi için bir yönetici tarafından onaylanması gerekiyor.",
|
||||||
"sshSettings": "SSH Ayarları",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI Ağ Geçidi",
|
||||||
"sshAccess": "SSH Erişimi",
|
"sshAccess": "SSH Erişimi",
|
||||||
"rdpSettings": "RDP Ayarları",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "VNC Ayarları",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH Sunucusu",
|
"sshServer": "SSH Sunucusu",
|
||||||
"rdpServer": "RDP Sunucusu",
|
"rdpServer": "RDP Sunucusu",
|
||||||
"vncServer": "VNC Sunucusu",
|
"vncServer": "VNC Sunucusu",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI Ağ Geçidi",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "Şema",
|
"editInternalResourceDialogScheme": "Şema",
|
||||||
"editInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir",
|
"editInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "ICMP Ping İzne Ver",
|
"privateResourceAllowIcmpPing": "ICMP Ping İzne Ver",
|
||||||
"privateResourceNetworkAccess": "Ağ Erişimi",
|
"privateResourceNetworkAccess": "Ağ Erişimi",
|
||||||
"privateResourceNetworkAccessDescription": "Bu kaynak için TCP/UDP port erişimini kontrol edin ve ICMP ping'in izin verilip verilmediğini kontrol edin.",
|
"privateResourceNetworkAccessDescription": "Bu kaynak için TCP/UDP port erişimini kontrol edin ve ICMP ping'in izin verilip verilmediğini kontrol edin.",
|
||||||
"hostSettings": "Sunucu Ayarları",
|
"hostSettings": "Sunucu",
|
||||||
"cidrSettings": "CIDR Ayarları",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
|
"createInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
|
||||||
"createInternalResourceDialogName": "Ad",
|
"createInternalResourceDialogName": "Ad",
|
||||||
"createInternalResourceDialogSite": "Site",
|
"createInternalResourceDialogSite": "Site",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI Ağ Geçidi",
|
||||||
"scheme": "Şema",
|
"scheme": "Şema",
|
||||||
"createInternalResourceDialogScheme": "Şema",
|
"createInternalResourceDialogScheme": "Şema",
|
||||||
"createInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir",
|
"createInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "Geçerli Şifre",
|
"validPassword": "Geçerli Şifre",
|
||||||
"validEmail": "Geçerli E-posta",
|
"validEmail": "Geçerli E-posta",
|
||||||
"validSSO": "Geçerli SSO",
|
"validSSO": "Geçerli SSO",
|
||||||
|
"validVirtualAPIKey": "Geçerli Sanal API Anahtarı",
|
||||||
"view": "Görüntüle",
|
"view": "Görüntüle",
|
||||||
"configManaged": "Yapılandırma Yönetildi",
|
"configManaged": "Yapılandırma Yönetildi",
|
||||||
"connectedClient": "Bağlı İstemci",
|
"connectedClient": "Bağlı İstemci",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "Kayıt Saklama",
|
"logRetention": "Kayıt Saklama",
|
||||||
"logRetentionDescription": "Bu organizasyon için farklı türdeki günlüklerin ne kadar süre saklanacağını yönetin veya devre dışı bırakın",
|
"logRetentionDescription": "Bu organizasyon için farklı türdeki günlüklerin ne kadar süre saklanacağını yönetin veya devre dışı bırakın",
|
||||||
"requestLogsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek günlüklerini görüntüleyin",
|
"requestLogsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek günlüklerini görüntüleyin",
|
||||||
|
"aiSessionLogs": "AI Ağ Geçidi Oturum Günlükleri",
|
||||||
|
"aiSessionLogsDescription": "Bu organizasyondaki AI ağ geçidi isteklerinin istem ve yanıt transkriptlerini görüntüleyin",
|
||||||
|
"sidebarLogsAi": "Oturum Günlükleri",
|
||||||
|
"commandLogsAi": "Oturum Günlükleri",
|
||||||
|
"sidebarLogsAiUsage": "Kullanım Analizi",
|
||||||
|
"commandLogsAiUsage": "Kullanım Analizi",
|
||||||
|
"provider": "Sağlayıcı",
|
||||||
|
"capability": "Yetenek",
|
||||||
|
"model": "Model",
|
||||||
|
"virtualApiKey": "Sanal API Anahtarı",
|
||||||
|
"noVirtualApiKey": "Sanal API anahtarı yok",
|
||||||
|
"stream": "Akış",
|
||||||
|
"streaming": "Akış",
|
||||||
|
"nonStreaming": "Akış Dışı",
|
||||||
|
"statusCode": "Durum Kodu",
|
||||||
|
"aiSessionId": "Oturum ID",
|
||||||
|
"aiSessionRequest": "İstek",
|
||||||
|
"aiSessionResponse": "Yanıt",
|
||||||
|
"aiSessionNoData": "Hiçbir veri yakalanmadı",
|
||||||
|
"aiSessionCouldNotParse": "(ham, döküm çözümlenemedi)",
|
||||||
|
"aiSessionLogTruncated": "Bu oturum saklama öncesi kısaltıldı ve eksik olabilir.",
|
||||||
|
"aiSessionViewRaw": "Ham JSON Görüntüle",
|
||||||
|
"aiSessionViewChat": "Sohbet Görüntüle",
|
||||||
|
"cost": "Maliyet",
|
||||||
|
"estimated": "Tahmini",
|
||||||
|
"tokenUsage": "Jeton Kullanımı",
|
||||||
|
"promptTokens": "Yazım Jetonları",
|
||||||
|
"cacheReadTokens": "Önbellek Okuma Jetonları",
|
||||||
|
"cacheWriteTokens": "Önbellek Yazma Jetonları",
|
||||||
|
"completionTokens": "Tamamlama Jetonları",
|
||||||
|
"reasoningTokens": "Muhakeme Jetonları",
|
||||||
|
"totalTokens": "Toplam Jetonlar",
|
||||||
"requestAnalyticsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek analizlerini görüntüleyin.",
|
"requestAnalyticsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek analizlerini görüntüleyin.",
|
||||||
"logRetentionRequestLabel": "HTTP İstek Günlüğü Saklama",
|
"logRetentionRequestLabel": "HTTP İstek Günlüğü Saklama",
|
||||||
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
|
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "Eylem günlüklerini ne kadar süre tutacağını belirle",
|
"logRetentionActionDescription": "Eylem günlüklerini ne kadar süre tutacağını belirle",
|
||||||
"logRetentionConnectionLabel": "Bağlantı kayıtlarını ne kadar süre saklayacağınız",
|
"logRetentionConnectionLabel": "Bağlantı kayıtlarını ne kadar süre saklayacağınız",
|
||||||
"logRetentionConnectionDescription": "Bağlantı kayıtlarını ne kadar süre saklayacağınız",
|
"logRetentionConnectionDescription": "Bağlantı kayıtlarını ne kadar süre saklayacağınız",
|
||||||
|
"logRetentionAISessionsLabel": "AI Ağ Geçidi Oturum Günlüğü Saklama Süresi",
|
||||||
|
"logRetentionAISessionsDescription": "AI ağ geçidi istem/yanıt oturum günlüklerinin ne kadar süreyle saklanacağını belirleyin",
|
||||||
"logRetentionDisabled": "Devre Dışı",
|
"logRetentionDisabled": "Devre Dışı",
|
||||||
"logRetention3Days": "3 gün",
|
"logRetention3Days": "3 gün",
|
||||||
"logRetention7Days": "7 gün",
|
"logRetention7Days": "7 gün",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "Kaynak Adresi",
|
"sourceAddress": "Kaynak Adresi",
|
||||||
"destinationAddress": "Hedef Adresi",
|
"destinationAddress": "Hedef Adresi",
|
||||||
"duration": "Süre",
|
"duration": "Süre",
|
||||||
"licenseRequiredToUse": "Bu özelliği kullanmak için bir <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisansı veya <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> gereklidir. <bookADemoLink>Tanıtım veya POC denemesi ayarlayın</bookADemoLink>",
|
"licenseRequiredToUse": "<enterpriseEditionLink>Kurumsal Sürüm lisansı</enterpriseEditionLink> gereklidir. <bookADemoLink>Bir demo veya deneme rezervasyonu yapın</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "Bu özelliği kullanmak için <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> gereklidir. Bu özellik ayrıca <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>'da da mevcuttur. <bookADemoLink>Tanıtım veya POC denemesi ayarlayın</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Kurumsal Sürüm lisansı</enterpriseEditionLink> gereklidir. <bookADemoLink>Bir demo veya deneme rezervasyonu yapın</bookADemoLink>",
|
||||||
"certResolver": "Sertifika Çözücü",
|
"certResolver": "Sertifika Çözücü",
|
||||||
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
|
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
|
||||||
"selectCertResolver": "Sertifika Çözücü Seçin",
|
"selectCertResolver": "Sertifika Çözücü Seçin",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "Birden fazla site seçmek, yüksek kullanılabilirlik için dirençli yönlendirme ve yedeklik sağlar.",
|
"internalResourceFormMultiSiteRoutingHelp": "Birden fazla site seçmek, yüksek kullanılabilirlik için dirençli yönlendirme ve yedeklik sağlar.",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Daha fazla bilgi",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Daha fazla bilgi",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "Belirtilen TCP/UDP portlarına erişimi kısıtlayın veya tüm portlara izin/engelleme verin.",
|
"editInternalResourceDialogPortRestrictionsDescription": "Belirtilen TCP/UDP portlarına erişimi kısıtlayın veya tüm portlara izin/engelleme verin.",
|
||||||
"createInternalResourceDialogHttpConfiguration": "HTTP yapılandırması",
|
"createInternalResourceDialogHttpConfiguration": "Alan Adı Yapılandırması",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "HTTP veya HTTPS üzerinden bu kaynağa ulaşmak için istemcilerin kullanacağı alan adını seçin.",
|
"createInternalResourceDialogHttpConfigurationDescription": "HTTP veya HTTPS üzerinden bu kaynağa ulaşmak için istemcilerin kullanacağı alan adını seçin.",
|
||||||
"editInternalResourceDialogHttpConfiguration": "HTTP yapılandırması",
|
"editInternalResourceDialogHttpConfiguration": "Alan Adı Yapılandırması",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "HTTP veya HTTPS üzerinden bu kaynağa ulaşmak için istemcilerin kullanacağı alan adını seçin.",
|
"editInternalResourceDialogHttpConfigurationDescription": "HTTP veya HTTPS üzerinden bu kaynağa ulaşmak için istemcilerin kullanacağı alan adını seçin.",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "Etiketsiz",
|
"resourceLauncherUnlabeled": "Etiketsiz",
|
||||||
|
"resourceLauncherAiGateway": "AI Ağ Geçidi",
|
||||||
"resourceLauncherNoSite": "Site Yok",
|
"resourceLauncherNoSite": "Site Yok",
|
||||||
|
"resourceLauncherAvailableModels": "Mevcut Modeller",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "Bu AI ağ geçidiyle kullanabileceğiniz modeller.",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "Bu kaynak için hiçbir model mevcut değil.",
|
||||||
|
"resourceLauncherAvailableModelsError": "Mevcut modeller yüklenemedi.",
|
||||||
|
"resourceLauncherApiKeys": "API Anahtarları",
|
||||||
|
"resourceLauncherApiKeysDescription": "Bu kaynakla kimlik doğrulamak için kimlik anahtarınızı veya atanmış bir anahtarı kullanın.",
|
||||||
|
"resourceLauncherApiKeysIdentity": "Kimlik Anahtarı",
|
||||||
|
"resourceLauncherApiKeysManual": "Atanmış Anahtarlar",
|
||||||
|
"resourceLauncherApiKeysEmpty": "Bu kaynak için hiçbir API anahtarı mevcut değil.",
|
||||||
|
"resourceLauncherApiKeysError": "API anahtarları yüklenemedi.",
|
||||||
"resourceLauncherNoResourcesInGroup": "Bu grupta kaynak yok",
|
"resourceLauncherNoResourcesInGroup": "Bu grupta kaynak yok",
|
||||||
"resourceLauncherEmptyStateTitle": "Kullanılabilir Kaynak Yok",
|
"resourceLauncherEmptyStateTitle": "Kullanılabilir Kaynak Yok",
|
||||||
"resourceLauncherEmptyStateDescription": "Henüz hiçbir kaynağa erişiminiz yok. Erişim istemek için yöneticinizle iletişime geçin.",
|
"resourceLauncherEmptyStateDescription": "Henüz hiçbir kaynağa erişiminiz yok. Erişim istemek için yöneticinizle iletişime geçin.",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "Başlatıcı görünümü silinemedi. Lütfen tekrar deneyin.",
|
"resourceLauncherViewDeleteFailedDescription": "Başlatıcı görünümü silinemedi. Lütfen tekrar deneyin.",
|
||||||
"memberPortalPrevious": "Önceki",
|
"memberPortalPrevious": "Önceki",
|
||||||
"memberPortalNext": "Sonraki",
|
"memberPortalNext": "Sonraki",
|
||||||
"httpSettings": "HTTP Ayarları",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "TCP Ayarları",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "UDP Ayarları",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "Güvenli bir bağlantı kuruluyor…",
|
"sshConnectingDescription": "Güvenli bir bağlantı kuruluyor…",
|
||||||
"sshConnecting": "Bağlanılıyor…",
|
"sshConnecting": "Bağlanılıyor…",
|
||||||
|
|||||||
+509
-13
@@ -43,6 +43,8 @@
|
|||||||
"inviteLoginUser": "请确保您以正确的用户登录。",
|
"inviteLoginUser": "请确保您以正确的用户登录。",
|
||||||
"inviteErrorNoUser": "很抱歉,但看起来你想访问的邀请不是一个存在的用户。",
|
"inviteErrorNoUser": "很抱歉,但看起来你想访问的邀请不是一个存在的用户。",
|
||||||
"inviteCreateUser": "请先创建一个帐户。",
|
"inviteCreateUser": "请先创建一个帐户。",
|
||||||
|
"inviteErrorOidcNotAllowed": "邀请只能由内部账户接受。退出登录,使用该电子邮件的密码登录。",
|
||||||
|
"inviteLoginInternalOnly": "邀请需要具有密码的内部账户。请创建账户或使用密码登录。",
|
||||||
"goHome": "返回首页",
|
"goHome": "返回首页",
|
||||||
"inviteLogInOtherUser": "以不同的用户登录",
|
"inviteLogInOtherUser": "以不同的用户登录",
|
||||||
"createAnAccount": "创建帐户",
|
"createAnAccount": "创建帐户",
|
||||||
@@ -286,7 +288,21 @@
|
|||||||
"noCountryFound": "找不到国家。",
|
"noCountryFound": "找不到国家。",
|
||||||
"siteSelectionDescription": "此站点将为目标提供连接。",
|
"siteSelectionDescription": "此站点将为目标提供连接。",
|
||||||
"resourceType": "资源类型",
|
"resourceType": "资源类型",
|
||||||
"resourceTypeDescription": "这会控制资源协议及其在浏览器中的渲染方式。之后不能更改。",
|
"resourceTypeDescription": "此功能控制资源协议,之后无法更改。",
|
||||||
|
"resourceTypeSearch": "搜索资源类型……",
|
||||||
|
"resourceTypeNotFound": "未找到资源类型",
|
||||||
|
"resourceTypeHttpDescription": "使用域名通过HTTPS代理网络流量",
|
||||||
|
"resourceTypeInferenceDescription": "通过附加的提供者处理AI请求",
|
||||||
|
"resourceTypeSshDescription": "从浏览器访问SSH服务器",
|
||||||
|
"resourceTypeRdpDescription": "从浏览器访问远程桌面",
|
||||||
|
"resourceTypeVncDescription": "从浏览器访问VNC桌面",
|
||||||
|
"resourceTypeTcpDescription": "使用端口号代理原始TCP流量",
|
||||||
|
"resourceTypeUdpDescription": "使用端口号代理原始UDP流量",
|
||||||
|
"privateResourceTypeDescription": "此功能控制客户端访问资源的方式,之后无法更改。",
|
||||||
|
"privateResourceTypeHostDescription": "在站点网络上向连接的客户端暴露单个主机",
|
||||||
|
"privateResourceTypeCidrDescription": "在站点网络上向连接的客户端暴露CIDR范围",
|
||||||
|
"privateResourceTypeHttpDescription": "通过域访问HTTP或HTTPS服务",
|
||||||
|
"privateResourceTypeSshDescription": "从连接的客户端访问SSH服务器",
|
||||||
"resourceDomainDescription": "资源将在此完全限定的域名上提供。",
|
"resourceDomainDescription": "资源将在此完全限定的域名上提供。",
|
||||||
"resourceHTTPSSettings": "HTTPS 设置",
|
"resourceHTTPSSettings": "HTTPS 设置",
|
||||||
"resourceHTTPSSettingsDescription": "配置如何通过 HTTPS 访问资源",
|
"resourceHTTPSSettingsDescription": "配置如何通过 HTTPS 访问资源",
|
||||||
@@ -855,12 +871,16 @@
|
|||||||
"authMethodsSave": "保存设置",
|
"authMethodsSave": "保存设置",
|
||||||
"policyAuthStackTitle": "身份验证",
|
"policyAuthStackTitle": "身份验证",
|
||||||
"policyAuthStackDescription": "控制哪些身份验证方法需由用户执行才能访问资源",
|
"policyAuthStackDescription": "控制哪些身份验证方法需由用户执行才能访问资源",
|
||||||
|
"policyAuthInferenceStackDescription": "选择哪些用户和角色可以对这个AI网关进行验证",
|
||||||
"policyAuthOrLogicTitle": "多种身份验证方法处于激活状态",
|
"policyAuthOrLogicTitle": "多种身份验证方法处于激活状态",
|
||||||
"policyAuthOrLogicBanner": "访问者可以使用下列任意激活的方法进行身份验证。无需完成全部过程。",
|
"policyAuthOrLogicBanner": "访问者可以使用下列任意激活的方法进行身份验证。无需完成全部过程。",
|
||||||
"policyAuthMethodActive": "激活",
|
"policyAuthMethodActive": "激活",
|
||||||
"policyAuthMethodOff": "关闭",
|
"policyAuthMethodOff": "关闭",
|
||||||
"policyAuthSsoTitle": "平台单点登录 SSO",
|
"policyAuthSsoTitle": "平台单点登录 SSO",
|
||||||
"policyAuthSsoDescription": "要求通过您组织的身份提供商登录",
|
"policyAuthSsoDescription": "要求通过您组织的身份提供商登录",
|
||||||
|
"policyAuthInferenceIdentityKeySignInUrl": "登录URL",
|
||||||
|
"policyAuthInferenceIdentityKeyHelpNoUrl": "每个用户已经拥有一个身份API密钥,因此您只需要为非用户客户端或共享访问创建虚拟API密钥。用户可以通过在此资源的URL中登录其身份提供者来检索其密钥,登录后将显示。",
|
||||||
|
"policyAuthInferenceIdentityKeyHelp": "每个用户已经拥有一个身份API密钥,因此您只需要为非用户客户端或共享访问创建虚拟API密钥。用户可在登录其身份提供者后在此处获取其密钥。",
|
||||||
"policyAuthSsoSummary": "{idp} · {users} 个用户, {roles} 个角色",
|
"policyAuthSsoSummary": "{idp} · {users} 个用户, {roles} 个角色",
|
||||||
"policyAuthSsoDefaultIdp": "默认提供商",
|
"policyAuthSsoDefaultIdp": "默认提供商",
|
||||||
"policyAuthAddDefaultIdentityProvider": "添加默认身份提供商",
|
"policyAuthAddDefaultIdentityProvider": "添加默认身份提供商",
|
||||||
@@ -1079,6 +1099,8 @@
|
|||||||
"accessRoleErrorNewRequired": "需要新角色",
|
"accessRoleErrorNewRequired": "需要新角色",
|
||||||
"accessRoleErrorRemove": "删除角色失败",
|
"accessRoleErrorRemove": "删除角色失败",
|
||||||
"accessRoleErrorRemoveDescription": "删除角色时出错。",
|
"accessRoleErrorRemoveDescription": "删除角色时出错。",
|
||||||
|
"accessRoleInferenceBudget": "AI预算",
|
||||||
|
"accessRoleInferenceBudgetDescription": "配置此角色成员如何根据支出或令牌限制来限制AI使用",
|
||||||
"accessRoleName": "角色名称",
|
"accessRoleName": "角色名称",
|
||||||
"accessRoleQuestionRemove": "您即将删除 `{name}` 角色。此操作无法撤销。",
|
"accessRoleQuestionRemove": "您即将删除 `{name}` 角色。此操作无法撤销。",
|
||||||
"accessRoleRemove": "删除角色",
|
"accessRoleRemove": "删除角色",
|
||||||
@@ -1424,6 +1446,28 @@
|
|||||||
"actionDeleteSite": "删除站点",
|
"actionDeleteSite": "删除站点",
|
||||||
"actionGetSite": "获取站点",
|
"actionGetSite": "获取站点",
|
||||||
"actionListSites": "站点列表",
|
"actionListSites": "站点列表",
|
||||||
|
"actionCreateAiProvider": "创建AI提供者",
|
||||||
|
"actionDeleteAiProvider": "删除AI提供者",
|
||||||
|
"actionGetAiProvider": "获取AI提供者",
|
||||||
|
"actionListAiProviders": "列出AI提供者",
|
||||||
|
"actionUpdateAiProvider": "更新AI提供者",
|
||||||
|
"actionCreateAiModel": "创建AI模型",
|
||||||
|
"actionDeleteAiModel": "删除AI模型",
|
||||||
|
"actionGetAiModel": "获取AI模型",
|
||||||
|
"actionListAiModels": "列出AI模型",
|
||||||
|
"actionUpdateAiModel": "更新AI模型",
|
||||||
|
"actionCreateAiBudget": "创建AI预算",
|
||||||
|
"actionDeleteAiBudget": "删除AI预算",
|
||||||
|
"actionGetAiBudget": "获取AI预算",
|
||||||
|
"actionListAiBudgets": "列出AI预算",
|
||||||
|
"actionUpdateAiBudget": "更新AI预算",
|
||||||
|
"actionListResourceAiModels": "列出资源AI模型",
|
||||||
|
"actionSetResourceAiModels": "设置资源AI模型",
|
||||||
|
"actionCreateVirtualApiKey": "创建虚拟API密钥",
|
||||||
|
"actionDeleteVirtualApiKey": "删除虚拟API密钥",
|
||||||
|
"actionGetVirtualApiKey": "获取虚拟API密钥",
|
||||||
|
"actionListVirtualApiKeys": "列出虚拟API密钥",
|
||||||
|
"actionUpdateVirtualApiKey": "更新虚拟API密钥",
|
||||||
"actionApplyBlueprint": "应用蓝图",
|
"actionApplyBlueprint": "应用蓝图",
|
||||||
"actionListBlueprints": "列表蓝图",
|
"actionListBlueprints": "列表蓝图",
|
||||||
"actionGetBlueprint": "获取蓝图",
|
"actionGetBlueprint": "获取蓝图",
|
||||||
@@ -1449,8 +1493,11 @@
|
|||||||
"actionSetResourcePincode": "设置资源粉码",
|
"actionSetResourcePincode": "设置资源粉码",
|
||||||
"actionSetResourceEmailWhitelist": "设置资源电子邮件白名单",
|
"actionSetResourceEmailWhitelist": "设置资源电子邮件白名单",
|
||||||
"actionGetResourceEmailWhitelist": "获取资源电子邮件白名单",
|
"actionGetResourceEmailWhitelist": "获取资源电子邮件白名单",
|
||||||
|
"actionListResourcePolicies": "列出资源策略",
|
||||||
|
"actionCreateResourcePolicy": "创建资源策略",
|
||||||
"actionGetResourcePolicy": "获取资源策略",
|
"actionGetResourcePolicy": "获取资源策略",
|
||||||
"actionUpdateResourcePolicy": "更新资源策略",
|
"actionUpdateResourcePolicy": "更新资源策略",
|
||||||
|
"actionDeleteResourcePolicy": "删除资源策略",
|
||||||
"actionSetResourcePolicyUsers": "设置资源策略用户",
|
"actionSetResourcePolicyUsers": "设置资源策略用户",
|
||||||
"actionSetResourcePolicyRoles": "设置资源策略角色",
|
"actionSetResourcePolicyRoles": "设置资源策略角色",
|
||||||
"actionSetResourcePolicyPassword": "设置资源策略密码",
|
"actionSetResourcePolicyPassword": "设置资源策略密码",
|
||||||
@@ -1622,6 +1669,404 @@
|
|||||||
"sidebarInvitations": "邀请",
|
"sidebarInvitations": "邀请",
|
||||||
"sidebarRoles": "角色",
|
"sidebarRoles": "角色",
|
||||||
"sidebarShareableLinks": "共享链接",
|
"sidebarShareableLinks": "共享链接",
|
||||||
|
"sidebarAiGateway": "AI网关",
|
||||||
|
"sidebarAiProviders": "提供者",
|
||||||
|
"commandAiProviders": "AI提供者",
|
||||||
|
"sidebarVirtualApiKeys": "虚拟API密钥",
|
||||||
|
"sidebarMyApiKeys": "您的API密钥",
|
||||||
|
"sidebarAccount": "启动器",
|
||||||
|
"commandVirtualApiKeys": "虚拟API密钥",
|
||||||
|
"virtualApiKeysTitle": "管理虚拟API密钥",
|
||||||
|
"virtualApiKeysDescription": "创建和管理AI网关访问到公共AI网关的手动API密钥",
|
||||||
|
"virtualApiKeysTabIdentity": "身份密钥",
|
||||||
|
"virtualApiKeysTabVirtual": "虚拟密钥",
|
||||||
|
"virtualApiKeysIdentitySplashTitle": "每个用户的身份密钥",
|
||||||
|
"virtualApiKeysIdentitySplashDescription": "每个用户已经拥有此组织的Pangolin身份密钥。它是唯一的,并能够认证他们访问AI网关。",
|
||||||
|
"virtualApiKeysIdentitySplashExample": "示例",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveTitle": "用户如何获取他们的密钥",
|
||||||
|
"virtualApiKeysIdentitySplashRetrieveResource": "访问公共AI网关URL并使用其账户登录。",
|
||||||
|
"virtualApiKeysIdentitySplashRetrievePage": "或在登录时访问<url></url> 。",
|
||||||
|
"virtualApiKeysIdentitySplashManual": "您可以在虚拟密钥标签中创建其他虚拟API密钥。那些密钥可以限定为特定的公共AI网关,并可以选择是否与用户相关联。创建密钥后立即授予对选定公共AI网关的访问权限。",
|
||||||
|
"virtualApiKeysIdentitySplashGoToVirtual": "手动创建密钥",
|
||||||
|
"virtualApiKeysEmailIdentity": "电子邮件身份密钥",
|
||||||
|
"virtualApiKeysEmailIdentityDescription": "向选择的用户或角色发送他们的Pangolin身份密钥。",
|
||||||
|
"virtualApiKeysEmailIdentitySendAll": "发送给所有用户",
|
||||||
|
"virtualApiKeysEmailIdentitySendAllDescription": "向所有有电子邮件账户的组织成员发送电子邮件。",
|
||||||
|
"virtualApiKeysEmailIdentitySelectUsers": "用户",
|
||||||
|
"virtualApiKeysEmailIdentitySelectRoles": "角色",
|
||||||
|
"virtualApiKeysEmailIdentitySubmit": "发送邮件",
|
||||||
|
"virtualApiKeysEmailIdentitySuccess": "身份密钥已发送",
|
||||||
|
"virtualApiKeysEmailIdentitySuccessDescription": "Sent {sent} emails.",
|
||||||
|
"virtualApiKeysEmailIdentitySkipped": "{skipped} users were skipped because they do not have an email address.",
|
||||||
|
"virtualApiKeysEmailIdentityRecipientsRequired": "至少选择一个用户或角色,或发送给所有用户。",
|
||||||
|
"virtualApiKeysEmailIdentityError": "发送身份密钥出错",
|
||||||
|
"virtualApiKeysEmailIdentityErrorDescription": "未能发送身份密钥邮件",
|
||||||
|
"virtualApiKeysBannerTitle": "每个用户的身份密钥",
|
||||||
|
"virtualApiKeysBannerDescription": "每个用户都可以在{keysUrl}获取一个身份密钥。您也可以在这里手动生成密钥以直接访问公共AI网关。",
|
||||||
|
"virtualApiKeysBannerButtonText": "查看身份密钥",
|
||||||
|
"virtualApiKeys": "虚拟API 密钥",
|
||||||
|
"virtualApiKeysSearch": "搜索密钥……",
|
||||||
|
"virtualApiKeysCreate": "创建虚拟API密钥",
|
||||||
|
"virtualApiKeysCreateDescription": "创建一个手动密钥,用于调用本组织内的公共AI网关",
|
||||||
|
"virtualApiKeysCreateButton": "创建密钥",
|
||||||
|
"virtualApiKeysEmpty": "尚无虚拟API密钥",
|
||||||
|
"virtualApiKeysName": "名称",
|
||||||
|
"virtualApiKeysDescriptionOptional": "描述(可选)",
|
||||||
|
"virtualApiKeysAssociateUserOptional": "关联用户(可选)",
|
||||||
|
"virtualApiKeysAssociateUserDescription": "将此密钥与用户相关联以跟踪使用情况。创建后,此密钥立即授予对所选公共AI网关的访问权限。无需手动将用户与资源相关联。该密钥还会在用户的个人资料中显示。",
|
||||||
|
"virtualApiKeysAllResources": "所有公共AI网关",
|
||||||
|
"virtualApiKeysAllResourcesDescription": "允许此密钥访问组织中的所有公共AI网关",
|
||||||
|
"virtualApiKeysSelectResources": "公共AI网关",
|
||||||
|
"virtualApiKeysSelectResourcesPlaceholder": "选择资源",
|
||||||
|
"virtualApiKeysSelectResourcesDescription": "选择此密钥可以访问的公共AI网关",
|
||||||
|
"virtualApiKeysNoResources": "无资源",
|
||||||
|
"virtualApiKeysSecret": "密钥",
|
||||||
|
"virtualApiKeysCopyKey": "复制此密钥。您可以稍后从表或编辑时查看。",
|
||||||
|
"virtualApiKeysViewSecret": "查看密钥",
|
||||||
|
"virtualApiKeysViewSecretTitle": "虚拟API密钥密钥",
|
||||||
|
"virtualApiKeysViewSecretDescription": "此密钥允许访问分配给此密钥的公共AI网关",
|
||||||
|
"virtualApiKeysEdit": "编辑虚拟API密钥",
|
||||||
|
"virtualApiKeysEditDescription": "更新该密钥的关联用户和公共AI网关访问",
|
||||||
|
"virtualApiKeysSaveButton": "保存更改",
|
||||||
|
"virtualApiKeysSelectResourcesRequired": "选择至少一个公共AI网关,或启用所有公共AI网关",
|
||||||
|
"virtualApiKeysUpdated": "虚拟API密钥已更新",
|
||||||
|
"virtualApiKeysUpdatedDescription": "虚拟API密钥已更新",
|
||||||
|
"virtualApiKeysErrorUpdate": "更新虚拟API密钥出错",
|
||||||
|
"virtualApiKeysErrorUpdateDescription": "未能更新虚拟API密钥",
|
||||||
|
"virtualApiKeysErrorCreate": "创建虚拟API密钥出错",
|
||||||
|
"virtualApiKeysErrorCreateDescription": "未能创建虚拟API密钥",
|
||||||
|
"virtualApiKeysErrorDelete": "删除虚拟API密钥出错",
|
||||||
|
"virtualApiKeysErrorDeleteMessage": "未能删除虚拟API密钥",
|
||||||
|
"virtualApiKeysDeleted": "虚拟API密钥已删除",
|
||||||
|
"virtualApiKeysDeletedDescription": "虚拟API密钥已删除",
|
||||||
|
"virtualApiKeysDelete": "删除虚拟API密钥",
|
||||||
|
"virtualApiKeysDeleteConfirm": "删除密钥",
|
||||||
|
"virtualApiKeysQuestionRemove": "您确定要删除此虚拟 API 密钥吗?",
|
||||||
|
"virtualApiKeysMessageRemove": "使用此密钥的客户端将立即失去访问权限。",
|
||||||
|
"virtualApiKeysErrorFetchSecret": "加载密钥出错",
|
||||||
|
"virtualApiKeysErrorFetchSecretDescription": "未能加载虚拟API密钥",
|
||||||
|
"virtualApiKeysFilterUnassigned": "未分配",
|
||||||
|
"virtualApiKeysInferenceBudget": "预算",
|
||||||
|
"virtualApiKeysInferenceBudgetDescription": "根据支出或令牌限制配置此密钥限制AI使用的方法",
|
||||||
|
"virtualApiKeysEmailOnGenerate": "生成密钥后发送邮件",
|
||||||
|
"virtualApiKeysEmailThisKey": "发送密钥邮件",
|
||||||
|
"virtualApiKeysEmailOnGenerateDescription": "密钥创建后将其发送给关联用户及其他地址",
|
||||||
|
"virtualApiKeysEmailThisKeyDescription": "将当前密钥发送给关联用户及其他地址",
|
||||||
|
"virtualApiKeysEmailSmtpRequired": "此服务器上未配置邮件",
|
||||||
|
"virtualApiKeysEmailSmtpRequiredDescription": "配置SMTP以发送虚拟API密钥。",
|
||||||
|
"virtualApiKeysEmailSendToUser": "发送给关联用户",
|
||||||
|
"virtualApiKeysEmailSendToUserDescription": "将密钥发送到与关联用户的账户邮箱",
|
||||||
|
"virtualApiKeysEmailSendToUserDisabled": "关联一个用户以将密钥发送给该用户",
|
||||||
|
"virtualApiKeysEmailAdditional": "附加电子邮件",
|
||||||
|
"virtualApiKeysEmailAdditionalPlaceholder": "添加电子邮件,然后按回车键",
|
||||||
|
"virtualApiKeysEmailRecipientsRequired": "选择关联用户,或至少添加一个电子邮件地址",
|
||||||
|
"myVirtualApiKeysTitle": "您的API密钥",
|
||||||
|
"myVirtualApiKeysDescription": "查看属于您的身份密钥和任何虚拟API密钥",
|
||||||
|
"myVirtualApiKeysResourceTitle": "与{resourceName}有关的您的API密钥",
|
||||||
|
"myVirtualApiKeysResourceDescription": "查看属于您的身份密钥和可访问{resourceName}的虚拟API密钥",
|
||||||
|
"myVirtualApiKeysIdentityTitle": "身份密钥",
|
||||||
|
"myVirtualApiKeysIdentityHeadline": "您的个人API密钥",
|
||||||
|
"myVirtualApiKeysIdentityDescription": "这是此组织的个人密钥。它是独一无二的并且用于在调用AI网关资源时识别您。",
|
||||||
|
"myVirtualApiKeysIdentityResourceHeadline": "与{resourceName}有关的您的个人API密钥",
|
||||||
|
"myVirtualApiKeysIdentityResourceDescription": "这是此组织的个人密钥。用于调用{resourceName}。",
|
||||||
|
"myVirtualApiKeysManualTitle": "分配的密钥",
|
||||||
|
"myVirtualApiKeysManualDescription": "管理员与您的账户相关的手动虚拟API密钥",
|
||||||
|
"myVirtualApiKeysManualResourceDescription": "手动虚拟API密钥与您的账户相关并可以访问{resourceName}",
|
||||||
|
"myVirtualApiKeysManualEmpty": "尚无分配的密钥",
|
||||||
|
"myVirtualApiKeysKindUser": "身份",
|
||||||
|
"myVirtualApiKeysKindManual": "手动",
|
||||||
|
"myVirtualApiKeysUnnamed": "未命名的密钥",
|
||||||
|
"myVirtualApiKeysRevealSecret": "显示密钥",
|
||||||
|
"myVirtualApiKeysViewSecretDescription": "此密钥认证您访问AI网关资源",
|
||||||
|
"aiProvidersTitle": "AI提供者",
|
||||||
|
"aiProvidersDescription": "连接模型提供者用于本组织的AI工作负载",
|
||||||
|
"aiProvidersBannerTitle": "连接模型提供者",
|
||||||
|
"aiProvidersBannerDescription": "提供商是Pangolin用于AI工作负载的模型后端。在此处连接OpenAI、Anthropic和其他提供者,然后将其附加到公共AI网关,以便用户可以进行身份识别访问、设置预算并记录日志。",
|
||||||
|
"aiProvidersAdd": "添加提供者",
|
||||||
|
"aiProvidersSearch": "搜索提供者……",
|
||||||
|
"aiProvidersEmpty": "尚无AI提供者",
|
||||||
|
"aiProviderCreate": "创建AI提供者",
|
||||||
|
"aiProviderCreateDescription": "为此组织添加一个模型提供者",
|
||||||
|
"aiProviderSeeAll": "查看所有提供者",
|
||||||
|
"aiProviderSetting": "{providerName}的提供者设置",
|
||||||
|
"aiProviderSettingDescription": "配置此AI提供者",
|
||||||
|
"aiProviderGeneral": "概览",
|
||||||
|
"aiProviderGeneralDescription": "此提供者的基础设置",
|
||||||
|
"aiProviderConfiguration": "配置",
|
||||||
|
"aiProviderConfigurationDescription": "此提供者的网络路由和认证",
|
||||||
|
"aiProviderNetworkSettings": "网络设置",
|
||||||
|
"aiProviderNetworkSettingsDescription": "选择流量到达此提供者的方式",
|
||||||
|
"aiProviderAuthSettings": "认证",
|
||||||
|
"aiProviderAuthSettingsDescription": "配置此提供者如何认证其上游URL的请求",
|
||||||
|
"aiProviderBudgetSettings": "预算",
|
||||||
|
"aiProviderBudgetSettingsDescription": "根据支出或令牌限制配置此提供者限制使用的方法",
|
||||||
|
"aiBudgetAdd": "添加预算",
|
||||||
|
"aiBudgetEmpty": "尚未设置预算。单击“添加预算”以设置支出或令牌限制。",
|
||||||
|
"aiBudgetUnit": "支出类型",
|
||||||
|
"aiBudgetPeriod": "重置周期",
|
||||||
|
"aiBudgetAmount": "最大发电",
|
||||||
|
"aiBudgetAmountPlaceholder": "最大发电",
|
||||||
|
"aiBudgetPeriodHourly": "每小时",
|
||||||
|
"aiBudgetPeriodDaily": "每日",
|
||||||
|
"aiBudgetPeriodWeekly": "每周",
|
||||||
|
"aiBudgetPeriodMonthly": "每月",
|
||||||
|
"aiBudgetPeriodYearly": "每年",
|
||||||
|
"aiBudgetPeriodLifetime": "终身",
|
||||||
|
"aiBudgetUnitUsd": "美元",
|
||||||
|
"aiBudgetUnitTokens": "令牌",
|
||||||
|
"aiBudgetConflictError": "此重置周期和支出类型的预算已存在",
|
||||||
|
"aiBudgetInvalidAmountError": "输入大于0的最大支出",
|
||||||
|
"aiBudgetUpdated": "预算已更新",
|
||||||
|
"aiBudgetErrorSave": "未能更新预算",
|
||||||
|
"aiProviderType": "提供者类型",
|
||||||
|
"aiProviderTypeSearch": "搜索提供者……",
|
||||||
|
"aiProviderTypeNotFound": "未找到提供者类型",
|
||||||
|
"aiProviderTypeOpenai": "OpenAI",
|
||||||
|
"aiProviderTypeAnthropic": "Anthropic",
|
||||||
|
"aiProviderTypeGoogleGemini": "Google Gemini",
|
||||||
|
"aiProviderTypeVertexAi": "Vertex AI",
|
||||||
|
"aiProviderTypeBedrock": "Amazon Bedrock",
|
||||||
|
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
|
||||||
|
"aiProviderTypeOpenRouter": "OpenRouter",
|
||||||
|
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
|
||||||
|
"aiProviderTypeCustom": "自定义",
|
||||||
|
"aiProviderTypeOpenaiDescription": "OpenAI API",
|
||||||
|
"aiProviderTypeAnthropicDescription": "Anthropic API",
|
||||||
|
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
|
||||||
|
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
|
||||||
|
"aiProviderTypeBedrockDescription": "Amazon Bedrock 运行时 API",
|
||||||
|
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
|
||||||
|
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
|
||||||
|
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
|
||||||
|
"aiProviderTypeCustomDescription": "使用您自己的端点或通过站点目标进行路由",
|
||||||
|
"aiProviderUpstreamUrl": "上游URL",
|
||||||
|
"aiProviderUpstreamUrlDescription": "提供商API的基础URL",
|
||||||
|
"aiProviderUpstreamUrlOptionalDescription": "留空以使用此提供商的默认上游URL",
|
||||||
|
"aiProviderEffectiveUpstreamUrl": "有效上游URL",
|
||||||
|
"aiProviderApiKey": "API密钥",
|
||||||
|
"aiProviderApiKeyDescription": "用于对请求进行身份验证的API密钥",
|
||||||
|
"aiProviderCustomHeadersDescription": "每次请求时发送的头部。换行分隔:头部名称:值",
|
||||||
|
"aiProviderApiKeyLastChars": "API密钥",
|
||||||
|
"aiProviderAuthType": "身份验证类型",
|
||||||
|
"aiProviderAuthTypeSearch": "搜索身份验证类型……",
|
||||||
|
"aiProviderAuthTypeNotFound": "未找到身份验证类型",
|
||||||
|
"aiProviderAuthTypeBearer": "Bearer",
|
||||||
|
"aiProviderAuthTypeBearerDescription": "授权:Bearer key。 由 OpenAI 及大多数提供商使用",
|
||||||
|
"aiProviderAuthTypeXApiKey": "x-api-key",
|
||||||
|
"aiProviderAuthTypeXApiKeyDescription": "x-api-key头部。由Anthropic使用",
|
||||||
|
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
|
||||||
|
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key头部。由Google Gemini使用",
|
||||||
|
"aiProviderAuthTypeHec": "Splunk HEC",
|
||||||
|
"aiProviderAuthTypeHecDescription": "授权:Splunk密钥。由Splunk HTTP事件收集器使用",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
|
||||||
|
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization:Bearer key。由Cloudflare AI Gateway使用",
|
||||||
|
"aiProviderAuthTypeNone": "无身份验证",
|
||||||
|
"aiProviderAuthTypePassthrough": "透传",
|
||||||
|
"aiProviderAuthTypeDescription": "上游API如何验证请求",
|
||||||
|
"aiProviderAuthTypePassthroughDescription": "将调用者的API密钥头部转发到上游",
|
||||||
|
"aiProviderAuthTypeNoneDescription": "不向上游发送身份验证头部",
|
||||||
|
"aiProviderRoutingMode": "路由模式",
|
||||||
|
"aiProviderRoutingModeDescription": "将流量发送到上游URL或您站点上的HTTP目标",
|
||||||
|
"aiProviderRoutingModeUrl": "上游URL",
|
||||||
|
"aiProviderRoutingModeUrlDescription": "调用公共或私人API基础URL",
|
||||||
|
"aiProviderRoutingModeTarget": "站点目标",
|
||||||
|
"aiProviderRoutingModeTargetDescription": "通过您站点上的目标进行路由",
|
||||||
|
"aiProviderRoutingModeTargetNote": "创建此提供商后,在“网络设置”选项卡中配置站点目标。",
|
||||||
|
"aiProviderTargetNoOne": "该提供商没有任何目标。 添加目标以通过您的站点路由请求。",
|
||||||
|
"aiProviderSkipTlsVerification": "跳过TLS验证",
|
||||||
|
"aiProviderSkipTlsVerificationDescription": "禁用上游连接的TLS证书验证",
|
||||||
|
"aiProviderBudget": "预算",
|
||||||
|
"aiProviderBudgetDescription": "此提供商的可选支出或令牌预算",
|
||||||
|
"aiProviderBudgetAmount": "预算金额",
|
||||||
|
"aiProviderBudgetUnit": "预算单位",
|
||||||
|
"aiProviderBudgetUnitUsd": "美元",
|
||||||
|
"aiProviderBudgetUnitTokens": "令牌",
|
||||||
|
"aiProviderEnabled": "已启用",
|
||||||
|
"aiProviderEnabledDescription": "彻底禁用此提供商的所有资源",
|
||||||
|
"aiProviderErrorCreate": "创建AI提供商失败",
|
||||||
|
"aiProviderErrorUpdate": "更新AI提供商失败",
|
||||||
|
"aiProviderErrorDelete": "删除AI提供商失败",
|
||||||
|
"aiProviderErrorLoad": "加载AI提供商失败",
|
||||||
|
"aiProviderErrorUpstreamUrlInvalid": "输入有效的上游URL",
|
||||||
|
"aiProviderErrorUpstreamUrlRequired": "此提供商需要上游URL",
|
||||||
|
"aiProviderErrorAuthTypeRequired": "需要身份验证类型",
|
||||||
|
"aiProviderErrorApiKeyRequired": "需要API密钥",
|
||||||
|
"aiProviderErrorRoutingModeTarget": "站点目标路由仅适用于自定义提供商",
|
||||||
|
"aiProviderErrorCapabilitiesRequired": "选择至少一个API功能",
|
||||||
|
"aiProviderCapabilities": "API功能",
|
||||||
|
"aiProviderCapabilitiesDescription": "选择此提供商可以处理的API格式。已知提供商从推荐的默认值开始。",
|
||||||
|
"aiProviderCapabilitiesCustomDescription": "选择此自定义提供程序可以处理的API格式",
|
||||||
|
"aiProviderCapabilitiesSelect": "选择功能",
|
||||||
|
"aiProviderCapabilitiesEmpty": "未找到功能",
|
||||||
|
"aiProviderCapabilitiesSearch": "搜索功能……",
|
||||||
|
"aiCapabilityOpenaiChat": "OpenAI 聊天完成",
|
||||||
|
"aiCapabilityOpenaiChatDescription": "支持 /v1/chat/completions",
|
||||||
|
"aiCapabilityOpenaiResponses": "OpenAI 回应",
|
||||||
|
"aiCapabilityOpenaiResponsesDescription": "支持 /v1/responses",
|
||||||
|
"aiCapabilityAnthropicMessages": "Anthropic 消息",
|
||||||
|
"aiCapabilityAnthropicMessagesDescription": "支持 /v1/messages",
|
||||||
|
"aiCapabilityGeminiGenerateContent": "Gemini 生成内容",
|
||||||
|
"aiCapabilityGeminiGenerateContentDescription": "支持直接Gemini API",
|
||||||
|
"aiCapabilityBedrockModelInvoke": "Bedrock 模型调用",
|
||||||
|
"aiCapabilityBedrockModelInvokeDescription": "支持 Amazon Bedrock InvokeModel",
|
||||||
|
"aiCapabilityGoogleGenerateContent": "Vertex 生成内容",
|
||||||
|
"aiCapabilityGoogleGenerateContentDescription": "支持 Vertex AI Gemini 格式",
|
||||||
|
"aiCapabilityGoogleRawPredict": "Vertex 原始预测",
|
||||||
|
"aiCapabilityGoogleRawPredictDescription": "支持 Vertex AI 原始预测适用于 Anthropic 模型",
|
||||||
|
"aiCapabilityBedrockConverse": "Bedrock 会话",
|
||||||
|
"aiCapabilityBedrockConverseDescription": "支持 Amazon Bedrock Converse API",
|
||||||
|
"aiProviderCreated": "AI 提供者已创建",
|
||||||
|
"aiProviderUpdated": "AI 提供者已更新",
|
||||||
|
"aiProviderDeleted": "AI 提供者已删除",
|
||||||
|
"aiProviderDelete": "删除提供者",
|
||||||
|
"aiProviderDeleteConfirm": "删除提供者",
|
||||||
|
"aiProviderQuestionRemove": "您确定要删除此AI提供者吗?",
|
||||||
|
"aiProviderMessageRemove": "这将永久删除提供者及其模型和目标。无法撤销。",
|
||||||
|
"aiProviderErrorNoUpdate": "AI 提供者不可用于更新",
|
||||||
|
"aiProviderModels": "模型",
|
||||||
|
"aiProviderModelsDescription": "为此提供者定义允许和拒绝列表。请求必须匹配允许条目,并且不得匹配拒绝条目。",
|
||||||
|
"aiProviderCreateModelsDescription": "选择该提供者可以服务的模型。空白允许列表会拒绝所有流量。您可以稍后添加或更改模型。",
|
||||||
|
"aiProviderModelsPlaceholder": "搜索模型或键入自定义密钥",
|
||||||
|
"aiProviderModelsAllow": "允许列表",
|
||||||
|
"aiProviderModelsAllowDescription": "可通过此提供者使用的模型。空白表示拒绝所有。",
|
||||||
|
"aiProviderModelsAllowPlaceholder": "输入模型密钥",
|
||||||
|
"aiProviderModelsAllowEmpty": "尚未添加任何模型。",
|
||||||
|
"aiProviderModelsBlock": "拒绝列表",
|
||||||
|
"aiProviderModelsBlockDescription": "即使匹配允许条目的模型也要拒绝。",
|
||||||
|
"aiProviderModelsBlockPlaceholder": "输入模型密钥",
|
||||||
|
"aiProviderModelsBlockEmpty": "尚未添加任何模型。",
|
||||||
|
"aiProviderModelsAdd": "添加模型",
|
||||||
|
"aiProviderModelsClearAll": "清除所有",
|
||||||
|
"aiProviderModelsAddCustom": "添加 \"{key}\"",
|
||||||
|
"aiProviderModelsAddCustomHint": "按 Enter 键以添加此自定义模型密钥。",
|
||||||
|
"aiProviderModelsAddBulk": "添加 {count} 个自定义密钥",
|
||||||
|
"aiProviderModelsAddBulkHint": "按 Enter 键以添加 {count} 个自定义密钥。",
|
||||||
|
"aiProviderModelsAddOne": "立即添加 {key}",
|
||||||
|
"aiProviderModelsAddSelected": "添加所选",
|
||||||
|
"aiProviderModelsSelectedCount": "{count} 已选择",
|
||||||
|
"aiProviderModelsSelectAll": "选择所有",
|
||||||
|
"aiProviderModelsClearSelected": "清除",
|
||||||
|
"aiProviderModelsBulkHint": "选择已知模型或键入自定义密钥。",
|
||||||
|
"aiProviderModelsCatalogEmpty": "没有匹配的目录模型。",
|
||||||
|
"aiProviderModelsCatalogHeading": "已知模型",
|
||||||
|
"aiProviderModelsAllLabel": "所有模型",
|
||||||
|
"aiProviderModelsAllPatternHint": "通配符: *",
|
||||||
|
"aiProviderModelsAddAllAllow": "允许所有模型",
|
||||||
|
"aiProviderModelsAddAllBlock": "拒绝所有模型",
|
||||||
|
"aiProviderModelsAddAllDescription": "使用 * 通配符,因此每个模型密钥都匹配。",
|
||||||
|
"aiProviderModelsViewMore": "查看更多 ({count})",
|
||||||
|
"aiProviderModelsViewLess": "查看更少",
|
||||||
|
"aiProviderModelsRemove": "删除模型",
|
||||||
|
"aiProviderModelsEditHint": "单击以编辑模型设置",
|
||||||
|
"aiProviderModelsSourceCatalog": "已知目录模型",
|
||||||
|
"aiProviderModelsSourceCustom": "自定义模型密钥",
|
||||||
|
"aiProviderModelsSourcePattern": "通配符模式",
|
||||||
|
"aiProviderModelsSourceAll": "匹配每个模型密钥",
|
||||||
|
"aiProviderModelsBudgetConfigured": "预算已配置",
|
||||||
|
"aiProviderModelsEditTitle": "编辑模型",
|
||||||
|
"aiProviderModelsEditDescription": "更新模型密钥或配置其预算。",
|
||||||
|
"aiProviderModelsBudgetTab": "预算",
|
||||||
|
"aiProviderModelsKeyLabel": "模型密钥",
|
||||||
|
"aiProviderModelsKeyRequired": "输入一个模型密钥",
|
||||||
|
"aiProviderModelsKeyDuplicate": "此模型密钥已在列表中",
|
||||||
|
"aiProviderModelsOverlapError": "这些模式不能同时在两个列表中: {keys}",
|
||||||
|
"aiProviderModelsUpdated": "模型已更新",
|
||||||
|
"aiProviderModelsErrorUpdate": "更新模型失败",
|
||||||
|
"aiResourceProviders": "提供者",
|
||||||
|
"aiResourceProvidersDescription": "选择此AI网关可使用的AI提供者",
|
||||||
|
"aiResourceProvidersHelp": "附加提供者并选择继承(使用每个提供者的列表)或选择(为此资源选择允许列表)。不允许附加提供者之间冲突的允许模式。",
|
||||||
|
"aiResourceProvidersSelect": "选择提供者",
|
||||||
|
"aiResourceProvidersEmpty": "未找到AI提供者",
|
||||||
|
"aiResourceProvidersNoneAttached": "尚未附加任何提供者。",
|
||||||
|
"aiResourceProvidersAdd": "添加提供者",
|
||||||
|
"aiResourceProvidersRemove": "删除提供者",
|
||||||
|
"aiResourceProviderToggleEnabled": "在资源上启用或禁用此提供者",
|
||||||
|
"aiResourceProviderDisabled": "已禁用",
|
||||||
|
"aiResourceProvidersUpdated": "提供者已更新",
|
||||||
|
"aiResourceProvidersErrorUpdate": "更新提供者失败",
|
||||||
|
"aiResourceProviderEditDescription": "选择如何在此资源上公开此提供者的模型。",
|
||||||
|
"viewProviderSettings": "查看提供者设置",
|
||||||
|
"aiResourceProviderMode": "访问模式",
|
||||||
|
"aiResourceProviderModeInherit": "继承",
|
||||||
|
"aiResourceProviderModeSelect": "选择",
|
||||||
|
"aiResourceProviderModeSelectSummary": "选择 · {count} 模型",
|
||||||
|
"aiResourceProviderModeInheritHelp": "按照提供者上配置的那样使用提供者的允许和拒绝列表。",
|
||||||
|
"aiResourceProviderModeSelectHelp": "为此资源选择提供者允许列表模型的子集。",
|
||||||
|
"aiResourceProviderAllowModels": "允许列表",
|
||||||
|
"aiResourceProviderAllowModelsSelect": "选择模型",
|
||||||
|
"aiResourceProviderAllowModelsSearch": "搜索模型……",
|
||||||
|
"aiResourceProviderAllowModelsEmpty": "未找到模型",
|
||||||
|
"aiResourceProviderAllowModelsHelp": "只有此提供者的允许列表中的模型可以被选择。",
|
||||||
|
"aiResourceAliasRequired": "AI网关需要别名",
|
||||||
|
"aiResourceDomainConfiguration": "域配置",
|
||||||
|
"aiResourceDomainConfigurationDescription": "选择客户端将用于访问此AI网关的域。",
|
||||||
|
"aiUsageAnalyticsTitle": "AI 使用分析",
|
||||||
|
"aiUsageAnalyticsDescription": "分析 AI 网关的成本、令牌使用情况以及在提供者、资源、角色和用户中的活动",
|
||||||
|
"aiUsageTabOverview": "概览",
|
||||||
|
"aiUsageTabProviders": "提供者",
|
||||||
|
"aiUsageTabResources": "资源",
|
||||||
|
"aiUsageFilterProvider": "提供者",
|
||||||
|
"aiUsageFilterModel": "模型",
|
||||||
|
"aiUsageFilterResource": "资源",
|
||||||
|
"aiUsageFilterRole": "角色",
|
||||||
|
"aiUsageFilterUser": "用户",
|
||||||
|
"aiUsageFilterAllProviders": "所有提供者",
|
||||||
|
"aiUsageFilterAllModels": "所有模型",
|
||||||
|
"aiUsageFilterAllResources": "所有资源",
|
||||||
|
"aiUsageFilterAllRoles": "所有角色",
|
||||||
|
"aiUsageFilterAllUsers": "所有用户",
|
||||||
|
"aiUsageFilterSearch": "搜索……",
|
||||||
|
"aiUsageFilterNotFound": "未找到选项",
|
||||||
|
"aiUsageResetFilters": "重置过滤器",
|
||||||
|
"aiUsageRefresh": "刷新",
|
||||||
|
"aiUsageTokenTypePrompt": "提示",
|
||||||
|
"aiUsageTokenTypeCacheRead": "缓存读取",
|
||||||
|
"aiUsageTokenTypeCacheWrite": "缓存写入",
|
||||||
|
"aiUsageTokenTypeCompletion": "完成",
|
||||||
|
"aiUsageTokenTypeReasoning": "推理",
|
||||||
|
"aiUsageRequests": "请求",
|
||||||
|
"aiUsageCost": "成本",
|
||||||
|
"aiUsageTokens": "令牌",
|
||||||
|
"aiUsageOther": "其他",
|
||||||
|
"aiUsageTotalRequests": "总请求数",
|
||||||
|
"aiUsageTotalTokens": "总令牌数",
|
||||||
|
"aiUsageTotalCost": "总成本",
|
||||||
|
"aiUsageEstimated": "估计",
|
||||||
|
"aiUsageRequestVolume": "请求量",
|
||||||
|
"aiUsageTokenUsage": "令牌使用情况",
|
||||||
|
"aiUsageModelCost": "模型成本",
|
||||||
|
"aiUsageModelTokens": "模型令牌",
|
||||||
|
"aiUsageTopModels": "热门模型",
|
||||||
|
"aiUsageTopProviders": "热门提供者",
|
||||||
|
"aiUsageProviderCost": "提供者成本",
|
||||||
|
"aiUsageProviderTokenUsage": "提供者令牌使用情况",
|
||||||
|
"aiUsageTopResources": "热门资源",
|
||||||
|
"aiUsageResourceCost": "资源成本",
|
||||||
|
"aiUsageResourceTokenUsage": "资源令牌使用情况",
|
||||||
|
"aiUsageResourceTypePublic": "公共资源",
|
||||||
|
"aiUsageResourceTypeSite": "私有资源",
|
||||||
|
"aiUsageNoResource": "没有资源",
|
||||||
|
"aiUsageRolesTab": "角色",
|
||||||
|
"aiUsageUsersTab": "用户",
|
||||||
|
"aiUsageTopRoles": "热门角色",
|
||||||
|
"aiUsageRoleCost": "角色成本",
|
||||||
|
"aiUsageRoleTokenUsage": "角色令牌使用情况",
|
||||||
|
"aiUsageTopUsers": "热门用户",
|
||||||
|
"aiUsageUserCost": "用户成本",
|
||||||
|
"aiUsageUserTokenUsage": "用户令牌使用情况",
|
||||||
|
"aiUsageUnknownUser": "未知用户",
|
||||||
|
"aiUsageVirtualApiKeysTab": "虚拟 API 密钥",
|
||||||
|
"aiUsageFilterVirtualApiKey": "虚拟 API 密钥",
|
||||||
|
"aiUsageFilterAllVirtualApiKeys": "所有虚拟 API 密钥",
|
||||||
|
"aiUsageTopVirtualApiKeys": "热门虚拟 API 密钥",
|
||||||
|
"aiUsageVirtualApiKeyCost": "虚拟 API 密钥成本",
|
||||||
|
"aiUsageVirtualApiKeyTokenUsage": "虚拟 API 密钥令牌使用情况",
|
||||||
|
"aiUsageUnknownVirtualApiKey": "无虚拟 API 密钥",
|
||||||
|
"aiUsageUnnamedVirtualApiKey": "未命名密钥",
|
||||||
|
"aiUsageLoading": "加载中……",
|
||||||
|
"aiUsageNoData": "没有数据",
|
||||||
|
"resourceBudgetSettings": "预算",
|
||||||
|
"resourceBudgetSettingsDescription": "配置此 AI 网关如何根据支出或令牌限制来限制使用",
|
||||||
"sidebarApiKeys": "API密钥",
|
"sidebarApiKeys": "API密钥",
|
||||||
"sidebarProvisioning": "预配",
|
"sidebarProvisioning": "预配",
|
||||||
"sidebarSettings": "设置",
|
"sidebarSettings": "设置",
|
||||||
@@ -2193,6 +2638,8 @@
|
|||||||
"subnetPlaceholder": "子网",
|
"subnetPlaceholder": "子网",
|
||||||
"addressDescription": "客户的内部地址。必须属于组织的子网。",
|
"addressDescription": "客户的内部地址。必须属于组织的子网。",
|
||||||
"selectSites": "选择站点",
|
"selectSites": "选择站点",
|
||||||
|
"selectResources": "选择资源",
|
||||||
|
"multiResourcesSelectorResourcesCount": "{count, plural, other {# 个资源}}",
|
||||||
"selectLabels": "选择标签",
|
"selectLabels": "选择标签",
|
||||||
"sitesDescription": "客户端将与所选站点进行连接",
|
"sitesDescription": "客户端将与所选站点进行连接",
|
||||||
"clientInstallOlm": "安装 Olm",
|
"clientInstallOlm": "安装 Olm",
|
||||||
@@ -2245,10 +2692,11 @@
|
|||||||
"timeIsInSeconds": "时间以秒为单位",
|
"timeIsInSeconds": "时间以秒为单位",
|
||||||
"requireDeviceApproval": "需要设备批准",
|
"requireDeviceApproval": "需要设备批准",
|
||||||
"requireDeviceApprovalDescription": "具有此角色的用户需要管理员批准的新设备才能连接和访问资源。",
|
"requireDeviceApprovalDescription": "具有此角色的用户需要管理员批准的新设备才能连接和访问资源。",
|
||||||
"sshSettings": "SSH 设置",
|
"sshSettings": "SSH",
|
||||||
|
"inferenceSettings": "AI 网关",
|
||||||
"sshAccess": "SSH 访问",
|
"sshAccess": "SSH 访问",
|
||||||
"rdpSettings": "RDP 设置",
|
"rdpSettings": "RDP",
|
||||||
"vncSettings": "VNC 设置",
|
"vncSettings": "VNC",
|
||||||
"sshServer": "SSH 服务器",
|
"sshServer": "SSH 服务器",
|
||||||
"rdpServer": "RDP 服务器",
|
"rdpServer": "RDP 服务器",
|
||||||
"vncServer": "VNC 服务器",
|
"vncServer": "VNC 服务器",
|
||||||
@@ -2401,6 +2849,7 @@
|
|||||||
"editInternalResourceDialogModeCidr": "CIDR",
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
"editInternalResourceDialogModeHttp": "HTTP",
|
"editInternalResourceDialogModeHttp": "HTTP",
|
||||||
"editInternalResourceDialogModeHttps": "HTTPS",
|
"editInternalResourceDialogModeHttps": "HTTPS",
|
||||||
|
"editInternalResourceDialogModeInference": "AI 网关",
|
||||||
"editInternalResourceDialogModeSsh": "SSH",
|
"editInternalResourceDialogModeSsh": "SSH",
|
||||||
"editInternalResourceDialogScheme": "方案",
|
"editInternalResourceDialogScheme": "方案",
|
||||||
"editInternalResourceDialogEnableSsl": "启用 TLS",
|
"editInternalResourceDialogEnableSsl": "启用 TLS",
|
||||||
@@ -2421,8 +2870,8 @@
|
|||||||
"privateResourceAllowIcmpPing": "允许 ICMP Ping",
|
"privateResourceAllowIcmpPing": "允许 ICMP Ping",
|
||||||
"privateResourceNetworkAccess": "网络访问",
|
"privateResourceNetworkAccess": "网络访问",
|
||||||
"privateResourceNetworkAccessDescription": "控制此资源的 TCP/UDP 端口访问以及是否允许 ICMP ping。",
|
"privateResourceNetworkAccessDescription": "控制此资源的 TCP/UDP 端口访问以及是否允许 ICMP ping。",
|
||||||
"hostSettings": "主机设置",
|
"hostSettings": "主机",
|
||||||
"cidrSettings": "CIDR 设置",
|
"cidrSettings": "CIDR",
|
||||||
"createInternalResourceDialogResourceProperties": "资源属性",
|
"createInternalResourceDialogResourceProperties": "资源属性",
|
||||||
"createInternalResourceDialogName": "名称",
|
"createInternalResourceDialogName": "名称",
|
||||||
"createInternalResourceDialogSite": "站点",
|
"createInternalResourceDialogSite": "站点",
|
||||||
@@ -2460,6 +2909,7 @@
|
|||||||
"createInternalResourceDialogModeHttp": "HTTP",
|
"createInternalResourceDialogModeHttp": "HTTP",
|
||||||
"createInternalResourceDialogModeHttps": "HTTPS",
|
"createInternalResourceDialogModeHttps": "HTTPS",
|
||||||
"createInternalResourceDialogModeSsh": "SSH",
|
"createInternalResourceDialogModeSsh": "SSH",
|
||||||
|
"createInternalResourceDialogModeInference": "AI 网关",
|
||||||
"scheme": "方案",
|
"scheme": "方案",
|
||||||
"createInternalResourceDialogScheme": "方案",
|
"createInternalResourceDialogScheme": "方案",
|
||||||
"createInternalResourceDialogEnableSsl": "启用 TLS",
|
"createInternalResourceDialogEnableSsl": "启用 TLS",
|
||||||
@@ -3051,6 +3501,7 @@
|
|||||||
"validPassword": "有效密码",
|
"validPassword": "有效密码",
|
||||||
"validEmail": "Valid email",
|
"validEmail": "Valid email",
|
||||||
"validSSO": "Valid SSO",
|
"validSSO": "Valid SSO",
|
||||||
|
"validVirtualAPIKey": "有效虚拟 API 密钥",
|
||||||
"view": "查看",
|
"view": "查看",
|
||||||
"configManaged": "配置已管理",
|
"configManaged": "配置已管理",
|
||||||
"connectedClient": "已连接客户端",
|
"connectedClient": "已连接客户端",
|
||||||
@@ -3072,6 +3523,38 @@
|
|||||||
"logRetention": "日志保留",
|
"logRetention": "日志保留",
|
||||||
"logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志",
|
"logRetentionDescription": "管理不同类型的日志为这个机构保留多长时间或禁用这些日志",
|
||||||
"requestLogsDescription": "查看此机构资源的详细请求日志",
|
"requestLogsDescription": "查看此机构资源的详细请求日志",
|
||||||
|
"aiSessionLogs": "AI 网关会话日志",
|
||||||
|
"aiSessionLogsDescription": "查看此组织中AI网关请求的提示和响应记录",
|
||||||
|
"sidebarLogsAi": "会话日志",
|
||||||
|
"commandLogsAi": "会话日志",
|
||||||
|
"sidebarLogsAiUsage": "使用分析",
|
||||||
|
"commandLogsAiUsage": "使用分析",
|
||||||
|
"provider": "提供者",
|
||||||
|
"capability": "能力",
|
||||||
|
"model": "模型",
|
||||||
|
"virtualApiKey": "虚拟 API 密钥",
|
||||||
|
"noVirtualApiKey": "无虚拟 API 密钥",
|
||||||
|
"stream": "流",
|
||||||
|
"streaming": "流媒体",
|
||||||
|
"nonStreaming": "非流",
|
||||||
|
"statusCode": "状态码",
|
||||||
|
"aiSessionId": "会话 ID",
|
||||||
|
"aiSessionRequest": "请求",
|
||||||
|
"aiSessionResponse": "响应",
|
||||||
|
"aiSessionNoData": "未捕获数据",
|
||||||
|
"aiSessionCouldNotParse": "(原始,无法解析记录)",
|
||||||
|
"aiSessionLogTruncated": "此会话在存储之前被截断,可能不完整。",
|
||||||
|
"aiSessionViewRaw": "查看原始 JSON",
|
||||||
|
"aiSessionViewChat": "查看聊天",
|
||||||
|
"cost": "成本",
|
||||||
|
"estimated": "估计",
|
||||||
|
"tokenUsage": "令牌使用情况",
|
||||||
|
"promptTokens": "提示令牌",
|
||||||
|
"cacheReadTokens": "缓存读取令牌",
|
||||||
|
"cacheWriteTokens": "缓存写入令牌",
|
||||||
|
"completionTokens": "完成令牌",
|
||||||
|
"reasoningTokens": "推理令牌",
|
||||||
|
"totalTokens": "总令牌数",
|
||||||
"requestAnalyticsDescription": "查看此机构资源的详细请求分析",
|
"requestAnalyticsDescription": "查看此机构资源的详细请求分析",
|
||||||
"logRetentionRequestLabel": "HTTP 请求日志保留",
|
"logRetentionRequestLabel": "HTTP 请求日志保留",
|
||||||
"logRetentionRequestDescription": "保留请求日志的时间",
|
"logRetentionRequestDescription": "保留请求日志的时间",
|
||||||
@@ -3081,6 +3564,8 @@
|
|||||||
"logRetentionActionDescription": "保留操作日志的时间",
|
"logRetentionActionDescription": "保留操作日志的时间",
|
||||||
"logRetentionConnectionLabel": "连接日志保留",
|
"logRetentionConnectionLabel": "连接日志保留",
|
||||||
"logRetentionConnectionDescription": "保留连接日志的时间",
|
"logRetentionConnectionDescription": "保留连接日志的时间",
|
||||||
|
"logRetentionAISessionsLabel": "AI 网关会话日志保留",
|
||||||
|
"logRetentionAISessionsDescription": "保留AI网关提示/响应会话日志的时间",
|
||||||
"logRetentionDisabled": "已禁用",
|
"logRetentionDisabled": "已禁用",
|
||||||
"logRetention3Days": "3 天",
|
"logRetention3Days": "3 天",
|
||||||
"logRetention7Days": "7 天",
|
"logRetention7Days": "7 天",
|
||||||
@@ -3098,8 +3583,8 @@
|
|||||||
"sourceAddress": "源地址",
|
"sourceAddress": "源地址",
|
||||||
"destinationAddress": "目的地址",
|
"destinationAddress": "目的地址",
|
||||||
"duration": "期限",
|
"duration": "期限",
|
||||||
"licenseRequiredToUse": "使用此功能需要<enterpriseLicenseLink>企业版</enterpriseLicenseLink>许可证或<pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>。<bookADemoLink>预约演示或POC试用以了解更多。</bookADemoLink>",
|
"licenseRequiredToUse": "需要<enterpriseEditionLink>企业版许可证</enterpriseEditionLink>。 <bookADemoLink>预约演示或试用</bookADemoLink>",
|
||||||
"ossEnterpriseEditionRequired": "需要<enterpriseEditionLink>企业版</enterpriseEditionLink>才能使用此功能。此功能也可在<pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>上获取。<bookADemoLink>预约演示或POC试用以了解更多。</bookADemoLink>",
|
"ossEnterpriseEditionRequired": "需要<enterpriseEditionLink>企业版许可证</enterpriseEditionLink>。 <bookADemoLink>预约演示或试用</bookADemoLink>",
|
||||||
"certResolver": "证书解决器",
|
"certResolver": "证书解决器",
|
||||||
"certResolverDescription": "选择用于此资源的证书解析器。",
|
"certResolverDescription": "选择用于此资源的证书解析器。",
|
||||||
"selectCertResolver": "选择证书解析",
|
"selectCertResolver": "选择证书解析",
|
||||||
@@ -3297,9 +3782,9 @@
|
|||||||
"internalResourceFormMultiSiteRoutingHelp": "选择多个站点可以实现高可用性的弹性路由和故障转移。",
|
"internalResourceFormMultiSiteRoutingHelp": "选择多个站点可以实现高可用性的弹性路由和故障转移。",
|
||||||
"internalResourceFormMultiSiteRoutingHelpLearnMore": "了解更多",
|
"internalResourceFormMultiSiteRoutingHelpLearnMore": "了解更多",
|
||||||
"editInternalResourceDialogPortRestrictionsDescription": "限制访问特定的TCP/UDP端口或允许/阻止所有端口。",
|
"editInternalResourceDialogPortRestrictionsDescription": "限制访问特定的TCP/UDP端口或允许/阻止所有端口。",
|
||||||
"createInternalResourceDialogHttpConfiguration": "HTTP 配置",
|
"createInternalResourceDialogHttpConfiguration": "域配置",
|
||||||
"createInternalResourceDialogHttpConfigurationDescription": "选择客户将使用的域名通过 HTTP 或 HTTPS 访问此资源。",
|
"createInternalResourceDialogHttpConfigurationDescription": "选择客户将使用的域名通过 HTTP 或 HTTPS 访问此资源。",
|
||||||
"editInternalResourceDialogHttpConfiguration": "HTTP 配置",
|
"editInternalResourceDialogHttpConfiguration": "域配置",
|
||||||
"editInternalResourceDialogHttpConfigurationDescription": "选择客户将使用的域名通过 HTTP 或 HTTPS 访问此资源。",
|
"editInternalResourceDialogHttpConfigurationDescription": "选择客户将使用的域名通过 HTTP 或 HTTPS 访问此资源。",
|
||||||
"editInternalResourceDialogTcp": "TCP",
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
"editInternalResourceDialogUdp": "UDP",
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
@@ -3735,7 +4220,18 @@
|
|||||||
"resourceLauncherTcp": "TCP",
|
"resourceLauncherTcp": "TCP",
|
||||||
"resourceLauncherUdp": "UDP",
|
"resourceLauncherUdp": "UDP",
|
||||||
"resourceLauncherUnlabeled": "未标记",
|
"resourceLauncherUnlabeled": "未标记",
|
||||||
|
"resourceLauncherAiGateway": "AI 网关",
|
||||||
"resourceLauncherNoSite": "无站点",
|
"resourceLauncherNoSite": "无站点",
|
||||||
|
"resourceLauncherAvailableModels": "可用模型",
|
||||||
|
"resourceLauncherAvailableModelsDescription": "您可以在此 AI 网关中使用的模型。",
|
||||||
|
"resourceLauncherAvailableModelsEmpty": "此资源没有可用的模型。",
|
||||||
|
"resourceLauncherAvailableModelsError": "无法加载可用模型。",
|
||||||
|
"resourceLauncherApiKeys": "API 密钥",
|
||||||
|
"resourceLauncherApiKeysDescription": "使用您的身份密钥或归属密钥进行身份验证。",
|
||||||
|
"resourceLauncherApiKeysIdentity": "身份密钥",
|
||||||
|
"resourceLauncherApiKeysManual": "归属密钥",
|
||||||
|
"resourceLauncherApiKeysEmpty": "此资源没有可用的 API 密钥。",
|
||||||
|
"resourceLauncherApiKeysError": "无法加载 API 密钥。",
|
||||||
"resourceLauncherNoResourcesInGroup": "此组中没有资源",
|
"resourceLauncherNoResourcesInGroup": "此组中没有资源",
|
||||||
"resourceLauncherEmptyStateTitle": "没有可用资源",
|
"resourceLauncherEmptyStateTitle": "没有可用资源",
|
||||||
"resourceLauncherEmptyStateDescription": "您还没有访问任何资源。请联系您的管理员以请求访问。",
|
"resourceLauncherEmptyStateDescription": "您还没有访问任何资源。请联系您的管理员以请求访问。",
|
||||||
@@ -3762,9 +4258,9 @@
|
|||||||
"resourceLauncherViewDeleteFailedDescription": "无法删除启动器视图。请再试一次。",
|
"resourceLauncherViewDeleteFailedDescription": "无法删除启动器视图。请再试一次。",
|
||||||
"memberPortalPrevious": "上一页",
|
"memberPortalPrevious": "上一页",
|
||||||
"memberPortalNext": "下一页",
|
"memberPortalNext": "下一页",
|
||||||
"httpSettings": "HTTP 设置",
|
"httpSettings": "HTTP",
|
||||||
"tcpSettings": "TCP 设置",
|
"tcpSettings": "TCP",
|
||||||
"udpSettings": "UDP 设置",
|
"udpSettings": "UDP",
|
||||||
"sshTitle": "SSH",
|
"sshTitle": "SSH",
|
||||||
"sshConnectingDescription": "正在建立安全连接…",
|
"sshConnectingDescription": "正在建立安全连接…",
|
||||||
"sshConnecting": "正在连接…",
|
"sshConnecting": "正在连接…",
|
||||||
|
|||||||
Generated
+223
-174
@@ -65,6 +65,7 @@
|
|||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"express-rate-limit": "8.5.2",
|
"express-rate-limit": "8.5.2",
|
||||||
"glob": "13.0.6",
|
"glob": "13.0.6",
|
||||||
|
"gpt-tokenizer": "^3.4.0",
|
||||||
"helmet": "8.2.0",
|
"helmet": "8.2.0",
|
||||||
"http-errors": "2.0.1",
|
"http-errors": "2.0.1",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
@@ -142,7 +143,7 @@
|
|||||||
"@types/yargs": "17.0.35",
|
"@types/yargs": "17.0.35",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"drizzle-kit": "0.31.10",
|
"drizzle-kit": "0.31.10",
|
||||||
"esbuild": "0.28.1",
|
"esbuild": "0.28.0",
|
||||||
"esbuild-node-externals": "1.22.0",
|
"esbuild-node-externals": "1.22.0",
|
||||||
"eslint": "10.4.0",
|
"eslint": "10.4.0",
|
||||||
"eslint-config-next": "16.2.6",
|
"eslint-config-next": "16.2.6",
|
||||||
@@ -1248,9 +1249,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -1265,9 +1266,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1282,9 +1283,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1299,9 +1300,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1316,9 +1317,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1333,9 +1334,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1350,9 +1351,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1367,9 +1368,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1384,9 +1385,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1401,9 +1402,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1418,9 +1419,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -1435,9 +1436,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -1452,9 +1453,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@@ -1469,9 +1470,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -1486,9 +1487,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -1503,9 +1504,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -1520,9 +1521,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1537,9 +1538,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1554,9 +1555,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1571,9 +1572,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1588,9 +1589,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1605,9 +1606,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1622,9 +1623,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1639,9 +1640,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1656,9 +1657,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -1673,9 +1674,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2076,6 +2077,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2092,6 +2096,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2108,6 +2115,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2124,6 +2134,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2140,6 +2153,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2172,6 +2188,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2204,6 +2223,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2226,6 +2248,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2248,6 +2273,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2270,6 +2298,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2292,6 +2323,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2336,6 +2370,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2911,6 +2948,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2927,6 +2967,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3164,6 +3207,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3180,6 +3226,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3540,6 +3589,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3560,6 +3612,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3580,6 +3635,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3600,6 +3658,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -7038,6 +7099,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -7054,6 +7118,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -7070,6 +7137,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -7086,6 +7156,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "Apache-2.0 AND MIT",
|
"license": "Apache-2.0 AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -7353,6 +7426,9 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -7370,6 +7446,9 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -7443,72 +7522,6 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"dev": true,
|
|
||||||
"inBundle": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/wasi-threads": "1.2.1",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"dev": true,
|
|
||||||
"inBundle": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"dev": true,
|
|
||||||
"inBundle": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"dev": true,
|
|
||||||
"inBundle": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@emnapi/core": "^1.7.1",
|
|
||||||
"@emnapi/runtime": "^1.7.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
|
||||||
"version": "0.10.1",
|
|
||||||
"dev": true,
|
|
||||||
"inBundle": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"dev": true,
|
|
||||||
"inBundle": true,
|
|
||||||
"license": "0BSD",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
|
||||||
@@ -8661,6 +8674,9 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8675,6 +8691,9 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8689,6 +8708,9 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8703,6 +8725,9 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8717,6 +8742,9 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8731,6 +8759,9 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -11469,9 +11500,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -11482,32 +11513,32 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/aix-ppc64": "0.28.1",
|
"@esbuild/aix-ppc64": "0.28.0",
|
||||||
"@esbuild/android-arm": "0.28.1",
|
"@esbuild/android-arm": "0.28.0",
|
||||||
"@esbuild/android-arm64": "0.28.1",
|
"@esbuild/android-arm64": "0.28.0",
|
||||||
"@esbuild/android-x64": "0.28.1",
|
"@esbuild/android-x64": "0.28.0",
|
||||||
"@esbuild/darwin-arm64": "0.28.1",
|
"@esbuild/darwin-arm64": "0.28.0",
|
||||||
"@esbuild/darwin-x64": "0.28.1",
|
"@esbuild/darwin-x64": "0.28.0",
|
||||||
"@esbuild/freebsd-arm64": "0.28.1",
|
"@esbuild/freebsd-arm64": "0.28.0",
|
||||||
"@esbuild/freebsd-x64": "0.28.1",
|
"@esbuild/freebsd-x64": "0.28.0",
|
||||||
"@esbuild/linux-arm": "0.28.1",
|
"@esbuild/linux-arm": "0.28.0",
|
||||||
"@esbuild/linux-arm64": "0.28.1",
|
"@esbuild/linux-arm64": "0.28.0",
|
||||||
"@esbuild/linux-ia32": "0.28.1",
|
"@esbuild/linux-ia32": "0.28.0",
|
||||||
"@esbuild/linux-loong64": "0.28.1",
|
"@esbuild/linux-loong64": "0.28.0",
|
||||||
"@esbuild/linux-mips64el": "0.28.1",
|
"@esbuild/linux-mips64el": "0.28.0",
|
||||||
"@esbuild/linux-ppc64": "0.28.1",
|
"@esbuild/linux-ppc64": "0.28.0",
|
||||||
"@esbuild/linux-riscv64": "0.28.1",
|
"@esbuild/linux-riscv64": "0.28.0",
|
||||||
"@esbuild/linux-s390x": "0.28.1",
|
"@esbuild/linux-s390x": "0.28.0",
|
||||||
"@esbuild/linux-x64": "0.28.1",
|
"@esbuild/linux-x64": "0.28.0",
|
||||||
"@esbuild/netbsd-arm64": "0.28.1",
|
"@esbuild/netbsd-arm64": "0.28.0",
|
||||||
"@esbuild/netbsd-x64": "0.28.1",
|
"@esbuild/netbsd-x64": "0.28.0",
|
||||||
"@esbuild/openbsd-arm64": "0.28.1",
|
"@esbuild/openbsd-arm64": "0.28.0",
|
||||||
"@esbuild/openbsd-x64": "0.28.1",
|
"@esbuild/openbsd-x64": "0.28.0",
|
||||||
"@esbuild/openharmony-arm64": "0.28.1",
|
"@esbuild/openharmony-arm64": "0.28.0",
|
||||||
"@esbuild/sunos-x64": "0.28.1",
|
"@esbuild/sunos-x64": "0.28.0",
|
||||||
"@esbuild/win32-arm64": "0.28.1",
|
"@esbuild/win32-arm64": "0.28.0",
|
||||||
"@esbuild/win32-ia32": "0.28.1",
|
"@esbuild/win32-ia32": "0.28.0",
|
||||||
"@esbuild/win32-x64": "0.28.1"
|
"@esbuild/win32-x64": "0.28.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild-node-externals": {
|
"node_modules/esbuild-node-externals": {
|
||||||
@@ -12760,6 +12791,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gpt-tokenizer": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@@ -13985,6 +14022,9 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -14006,6 +14046,9 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -15213,6 +15256,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -15229,6 +15275,9 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
+4
-3
@@ -33,9 +33,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "8.5.0",
|
"@asteasolutions/zod-to-openapi": "8.5.0",
|
||||||
|
"@aws-sdk/client-s3": "3.1056.0",
|
||||||
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
|
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
|
||||||
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
|
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
|
||||||
"@aws-sdk/client-s3": "3.1056.0",
|
|
||||||
"@headlessui/react": "2.2.10",
|
"@headlessui/react": "2.2.10",
|
||||||
"@hookform/resolvers": "5.4.0",
|
"@hookform/resolvers": "5.4.0",
|
||||||
"@monaco-editor/react": "4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
@@ -88,6 +88,7 @@
|
|||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"express-rate-limit": "8.5.2",
|
"express-rate-limit": "8.5.2",
|
||||||
"glob": "13.0.6",
|
"glob": "13.0.6",
|
||||||
|
"gpt-tokenizer": "^3.4.0",
|
||||||
"helmet": "8.2.0",
|
"helmet": "8.2.0",
|
||||||
"http-errors": "2.0.1",
|
"http-errors": "2.0.1",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
@@ -165,7 +166,7 @@
|
|||||||
"@types/yargs": "17.0.35",
|
"@types/yargs": "17.0.35",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"drizzle-kit": "0.31.10",
|
"drizzle-kit": "0.31.10",
|
||||||
"esbuild": "0.28.1",
|
"esbuild": "0.28.0",
|
||||||
"esbuild-node-externals": "1.22.0",
|
"esbuild-node-externals": "1.22.0",
|
||||||
"eslint": "10.4.0",
|
"eslint": "10.4.0",
|
||||||
"eslint-config-next": "16.2.6",
|
"eslint-config-next": "16.2.6",
|
||||||
@@ -179,7 +180,7 @@
|
|||||||
"typescript-eslint": "8.60.0"
|
"typescript-eslint": "8.60.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"esbuild": "0.28.1",
|
"esbuild": "0.28.0",
|
||||||
"dompurify": "3.4.0",
|
"dompurify": "3.4.0",
|
||||||
"postcss": "8.5.15"
|
"postcss": "8.5.15"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import express from "express";
|
||||||
|
import helmet from "helmet";
|
||||||
|
import cors from "cors";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import {
|
||||||
|
errorHandlerMiddleware,
|
||||||
|
notFoundMiddleware
|
||||||
|
} from "@server/middlewares";
|
||||||
|
import { createAiGatewayRouter } from "@server/routers/aiGateway";
|
||||||
|
|
||||||
|
const aiGatewayPort = config.getRawConfig().server.ai_gateway_port;
|
||||||
|
|
||||||
|
export function createAiGatewayServer() {
|
||||||
|
const aiGatewayServer = express();
|
||||||
|
|
||||||
|
const trustProxy = config.getRawConfig().server.trust_proxy;
|
||||||
|
if (trustProxy) {
|
||||||
|
aiGatewayServer.set("trust proxy", trustProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
aiGatewayServer.use(helmet());
|
||||||
|
aiGatewayServer.use(cors());
|
||||||
|
aiGatewayServer.use(express.json());
|
||||||
|
|
||||||
|
aiGatewayServer.use(createAiGatewayRouter());
|
||||||
|
|
||||||
|
aiGatewayServer.use(notFoundMiddleware);
|
||||||
|
aiGatewayServer.use(errorHandlerMiddleware);
|
||||||
|
|
||||||
|
aiGatewayServer.listen(aiGatewayPort, (err?: any) => {
|
||||||
|
if (err) throw err;
|
||||||
|
logger.info(
|
||||||
|
`AI gateway server is running on http://localhost:${aiGatewayPort}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return aiGatewayServer;
|
||||||
|
}
|
||||||
+23
-1
@@ -50,6 +50,8 @@ export enum ActionsEnum {
|
|||||||
setResourceUsers = "setResourceUsers",
|
setResourceUsers = "setResourceUsers",
|
||||||
setResourceRoles = "setResourceRoles",
|
setResourceRoles = "setResourceRoles",
|
||||||
listResourceUsers = "listResourceUsers",
|
listResourceUsers = "listResourceUsers",
|
||||||
|
listResourceAiModels = "listResourceAiModels",
|
||||||
|
setResourceAiModels = "setResourceAiModels",
|
||||||
// removeRoleSite = "removeRoleSite",
|
// removeRoleSite = "removeRoleSite",
|
||||||
// addRoleAction = "addRoleAction",
|
// addRoleAction = "addRoleAction",
|
||||||
// removeRoleAction = "removeRoleAction",
|
// removeRoleAction = "removeRoleAction",
|
||||||
@@ -182,7 +184,27 @@ export enum ActionsEnum {
|
|||||||
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
||||||
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
||||||
setResourcePolicyRules = "setResourcePolicyRules",
|
setResourcePolicyRules = "setResourcePolicyRules",
|
||||||
createOrgWideLauncherView = "createOrgWideLauncherView"
|
createOrgWideLauncherView = "createOrgWideLauncherView",
|
||||||
|
createAiProvider = "createAiProvider",
|
||||||
|
deleteAiProvider = "deleteAiProvider",
|
||||||
|
getAiProvider = "getAiProvider",
|
||||||
|
listAiProviders = "listAiProviders",
|
||||||
|
updateAiProvider = "updateAiProvider",
|
||||||
|
createAiModel = "createAiModel",
|
||||||
|
deleteAiModel = "deleteAiModel",
|
||||||
|
getAiModel = "getAiModel",
|
||||||
|
listAiModels = "listAiModels",
|
||||||
|
updateAiModel = "updateAiModel",
|
||||||
|
createAiBudget = "createAiBudget",
|
||||||
|
deleteAiBudget = "deleteAiBudget",
|
||||||
|
getAiBudget = "getAiBudget",
|
||||||
|
listAiBudgets = "listAiBudgets",
|
||||||
|
updateAiBudget = "updateAiBudget",
|
||||||
|
createVirtualApiKey = "createVirtualApiKey",
|
||||||
|
deleteVirtualApiKey = "deleteVirtualApiKey",
|
||||||
|
getVirtualApiKey = "getVirtualApiKey",
|
||||||
|
listVirtualApiKeys = "listVirtualApiKeys",
|
||||||
|
updateVirtualApiKey = "updateVirtualApiKey"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
users,
|
||||||
|
virtualApiKeyResources,
|
||||||
|
virtualApiKeys,
|
||||||
|
type VirtualApiKey
|
||||||
|
} from "@server/db";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import {
|
||||||
|
decryptVirtualApiKeyToken,
|
||||||
|
VIRTUAL_API_KEY_PREFIX,
|
||||||
|
looksLikeVirtualApiKeyCredential
|
||||||
|
} from "@server/lib/virtualApiKey";
|
||||||
|
import { getUserOrgRoles } from "@server/lib/userOrgRoles";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { isWithinExpirationDate } from "oslo";
|
||||||
|
|
||||||
|
export type VirtualApiKeyCredential = {
|
||||||
|
virtualApiKeyId: string;
|
||||||
|
secret: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VirtualApiKeyUserData = {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
email: string | null;
|
||||||
|
name: string | null;
|
||||||
|
role: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getHeader(
|
||||||
|
headers: Record<string, string> | undefined,
|
||||||
|
name: string
|
||||||
|
): string | undefined {
|
||||||
|
if (!headers) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (headers[name] !== undefined) {
|
||||||
|
return headers[name];
|
||||||
|
}
|
||||||
|
const lower = name.toLowerCase();
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
if (key.toLowerCase() === lower) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVkCredential(
|
||||||
|
raw: string | undefined
|
||||||
|
): VirtualApiKeyCredential | null {
|
||||||
|
if (!raw || !looksLikeVirtualApiKeyCredential(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const withoutPrefix = raw.trim().slice(VIRTUAL_API_KEY_PREFIX.length);
|
||||||
|
const dot = withoutPrefix.indexOf(".");
|
||||||
|
return {
|
||||||
|
virtualApiKeyId: withoutPrefix.slice(0, dot),
|
||||||
|
secret: withoutPrefix.slice(dot + 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractVirtualApiKeyCredential(
|
||||||
|
headers: Record<string, string> | undefined
|
||||||
|
): VirtualApiKeyCredential | null {
|
||||||
|
if (!headers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorization = getHeader(headers, "authorization");
|
||||||
|
if (authorization) {
|
||||||
|
const bearerMatch = authorization.match(/^Bearer\s+(.+)$/i);
|
||||||
|
if (bearerMatch) {
|
||||||
|
const credential = parseVkCredential(bearerMatch[1]);
|
||||||
|
if (credential) {
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const splunkMatch = authorization.match(/^Splunk\s+(.+)$/i);
|
||||||
|
if (splunkMatch) {
|
||||||
|
const credential = parseVkCredential(splunkMatch[1]);
|
||||||
|
if (credential) {
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfAig = getHeader(headers, "cf-aig-authorization");
|
||||||
|
if (cfAig) {
|
||||||
|
const bearerMatch = cfAig.match(/^Bearer\s+(.+)$/i);
|
||||||
|
const credential = parseVkCredential(
|
||||||
|
bearerMatch ? bearerMatch[1] : cfAig
|
||||||
|
);
|
||||||
|
if (credential) {
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of ["x-api-key", "x-goog-api-key"] as const) {
|
||||||
|
const credential = parseVkCredential(getHeader(headers, name));
|
||||||
|
if (credential) {
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildUserData(
|
||||||
|
userId: string,
|
||||||
|
orgId: string
|
||||||
|
): Promise<VirtualApiKeyUserData | undefined> {
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.userId, userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.getRawConfig().flags?.require_email_verification &&
|
||||||
|
!user.emailVerified
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
|
||||||
|
if (userOrgRoles.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userHasResourceAccess(
|
||||||
|
userId: string,
|
||||||
|
resourceId: number,
|
||||||
|
orgId: string
|
||||||
|
): Promise<{ allowed: boolean; userData?: VirtualApiKeyUserData }> {
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.userId, userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return { allowed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.getRawConfig().flags?.require_email_verification &&
|
||||||
|
!user.emailVerified
|
||||||
|
) {
|
||||||
|
return { allowed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
|
||||||
|
if (userOrgRoles.length === 0) {
|
||||||
|
return { allowed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = await canUserAccessResource({
|
||||||
|
userId,
|
||||||
|
resourceId,
|
||||||
|
roleIds: userOrgRoles.map((r) => r.roleId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
return { allowed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
userData: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function manualKeyHasResourceAccess(
|
||||||
|
key: VirtualApiKey,
|
||||||
|
resourceId: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (key.allResources) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ resourceId: virtualApiKeyResources.resourceId })
|
||||||
|
.from(virtualApiKeyResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(virtualApiKeyResources.virtualApiKeyId, key.virtualApiKeyId),
|
||||||
|
eq(virtualApiKeyResources.resourceId, resourceId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return Boolean(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function touchLastUsedAt(virtualApiKeyId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.update(virtualApiKeys)
|
||||||
|
.set({ lastUsedAt: Date.now() })
|
||||||
|
.where(eq(virtualApiKeys.virtualApiKeyId, virtualApiKeyId));
|
||||||
|
} catch {
|
||||||
|
// Best-effort; do not fail auth on audit timestamp updates.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyVirtualApiKey({
|
||||||
|
credential,
|
||||||
|
resourceId,
|
||||||
|
orgId
|
||||||
|
}: {
|
||||||
|
credential: VirtualApiKeyCredential;
|
||||||
|
resourceId: number;
|
||||||
|
orgId: string;
|
||||||
|
}): Promise<{
|
||||||
|
valid: boolean;
|
||||||
|
error?: string;
|
||||||
|
key?: VirtualApiKey;
|
||||||
|
userData?: VirtualApiKeyUserData;
|
||||||
|
}> {
|
||||||
|
const [key] = await db
|
||||||
|
.select()
|
||||||
|
.from(virtualApiKeys)
|
||||||
|
.where(eq(virtualApiKeys.virtualApiKeyId, credential.virtualApiKeyId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return { valid: false, error: "Virtual API key not found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.orgId !== orgId) {
|
||||||
|
return { valid: false, error: "Virtual API key org mismatch" };
|
||||||
|
}
|
||||||
|
|
||||||
|
let plaintext: string;
|
||||||
|
try {
|
||||||
|
plaintext = decryptVirtualApiKeyToken(key.token);
|
||||||
|
} catch {
|
||||||
|
return { valid: false, error: "Virtual API key secret is invalid" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plaintext !== credential.secret) {
|
||||||
|
return { valid: false, error: "Invalid virtual API key secret" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.expiresAt && !isWithinExpirationDate(new Date(key.expiresAt))) {
|
||||||
|
return { valid: false, error: "Virtual API key has expired" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.kind === "manual") {
|
||||||
|
const scoped = await manualKeyHasResourceAccess(key, resourceId);
|
||||||
|
if (!scoped) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: "Virtual API key is not scoped to this resource"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let userData: VirtualApiKeyUserData | undefined;
|
||||||
|
if (key.userId) {
|
||||||
|
userData = await buildUserData(key.userId, orgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await touchLastUsedAt(key.virtualApiKeyId);
|
||||||
|
return { valid: true, key, userData };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.kind === "user") {
|
||||||
|
if (!key.userId) {
|
||||||
|
return { valid: false, error: "User virtual API key has no user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const access = await userHasResourceAccess(
|
||||||
|
key.userId,
|
||||||
|
resourceId,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
|
if (!access.allowed || !access.userData) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: "User is not allowed to access this resource"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await touchLastUsedAt(key.virtualApiKeyId);
|
||||||
|
return { valid: true, key, userData: access.userData };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: false, error: "Unknown virtual API key kind" };
|
||||||
|
}
|
||||||
@@ -3,12 +3,16 @@ import { flushConnectionLogToDb } from "#dynamic/routers/newt";
|
|||||||
import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth";
|
import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth";
|
||||||
import { stopPingAccumulator } from "@server/routers/newt/pingAccumulator";
|
import { stopPingAccumulator } from "@server/routers/newt/pingAccumulator";
|
||||||
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
|
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
|
||||||
|
import { shutdownUsageRecorder } from "@server/lib/aiBudgetEnforcement";
|
||||||
|
import { shutdownAiSessionLogger } from "@server/routers/aiGateway/logAiSession";
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
await stopPingAccumulator();
|
await stopPingAccumulator();
|
||||||
await flushBandwidthToDb();
|
await flushBandwidthToDb();
|
||||||
await flushConnectionLogToDb();
|
await flushConnectionLogToDb();
|
||||||
await flushSiteBandwidthToDb();
|
await flushSiteBandwidthToDb();
|
||||||
|
await shutdownUsageRecorder();
|
||||||
|
await shutdownAiSessionLogger();
|
||||||
await wsCleanup();
|
await wsCleanup();
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import {
|
import {
|
||||||
|
aiProviders,
|
||||||
clients,
|
clients,
|
||||||
db,
|
db,
|
||||||
resourcePolicies,
|
resourcePolicies,
|
||||||
@@ -113,6 +114,32 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUniqueProviderName(orgId: string): Promise<string> {
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = generateName();
|
||||||
|
|
||||||
|
const aiProviderCount = await db
|
||||||
|
.select({
|
||||||
|
niceId: aiProviders.niceId,
|
||||||
|
orgId: aiProviders.orgId
|
||||||
|
})
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(eq(aiProviders.niceId, name), eq(aiProviders.orgId, orgId))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (aiProviderCount.length === 0) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUniqueResourcePolicyName(
|
export async function getUniqueResourcePolicyName(
|
||||||
orgId: string
|
orgId: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
|||||||
@@ -29,25 +29,6 @@ import {
|
|||||||
labels
|
labels
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
|
|
||||||
export const certificates = pgTable("certificates", {
|
|
||||||
certId: serial("certId").primaryKey(),
|
|
||||||
domain: varchar("domain", { length: 255 }).notNull().unique(),
|
|
||||||
domainId: varchar("domainId").references(() => domains.domainId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
}),
|
|
||||||
wildcard: boolean("wildcard").default(false),
|
|
||||||
status: varchar("status", { length: 50 }).notNull().default("pending"), // pending, requested, valid, expired, failed
|
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }),
|
|
||||||
lastRenewalAttempt: bigint("lastRenewalAttempt", { mode: "number" }),
|
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
|
||||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull(),
|
|
||||||
orderId: varchar("orderId", { length: 500 }),
|
|
||||||
errorMessage: text("errorMessage"),
|
|
||||||
renewalCount: integer("renewalCount").default(0),
|
|
||||||
certFile: text("certFile"),
|
|
||||||
keyFile: text("keyFile")
|
|
||||||
});
|
|
||||||
|
|
||||||
export const dnsChallenge = pgTable("dnsChallenges", {
|
export const dnsChallenge = pgTable("dnsChallenges", {
|
||||||
dnsChallengeId: serial("dnsChallengeId").primaryKey(),
|
dnsChallengeId: serial("dnsChallengeId").primaryKey(),
|
||||||
domain: varchar("domain", { length: 255 }).notNull(),
|
domain: varchar("domain", { length: 255 }).notNull(),
|
||||||
@@ -95,7 +76,8 @@ export const subscriptions = pgTable("subscriptions", {
|
|||||||
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }),
|
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }),
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }),
|
expiresAt: bigint("expiresAt", { mode: "number" }),
|
||||||
trial: boolean("trial").default(false),
|
trial: boolean("trial").default(false),
|
||||||
type: varchar("type", { length: 50 }) // tier1, tier2, tier3, or license
|
type: varchar("type", { length: 50 }), // tier1, tier2, tier3, or license
|
||||||
|
override: boolean("override").default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const subscriptionItems = pgTable("subscriptionItems", {
|
export const subscriptionItems = pgTable("subscriptionItems", {
|
||||||
@@ -632,7 +614,6 @@ export const trialNotifications = pgTable("trialNotifications", {
|
|||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
|
||||||
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
||||||
export type Customer = InferSelectModel<typeof customers>;
|
export type Customer = InferSelectModel<typeof customers>;
|
||||||
export type Subscription = InferSelectModel<typeof subscriptions>;
|
export type Subscription = InferSelectModel<typeof subscriptions>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { InferSelectModel, sql } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
bigint,
|
bigint,
|
||||||
boolean,
|
boolean,
|
||||||
|
check,
|
||||||
index,
|
index,
|
||||||
integer,
|
integer,
|
||||||
pgTable,
|
pgTable,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
serial,
|
serial,
|
||||||
text,
|
text,
|
||||||
unique,
|
unique,
|
||||||
|
uniqueIndex,
|
||||||
varchar
|
varchar
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
@@ -63,6 +65,11 @@ export const orgs = pgTable("orgs", {
|
|||||||
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
|
settingsLogRetentionDaysAISessions: integer(
|
||||||
|
"settingsLogRetentionDaysAISessions"
|
||||||
|
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(7),
|
||||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||||
isBillingOrg: boolean("isBillingOrg"),
|
isBillingOrg: boolean("isBillingOrg"),
|
||||||
@@ -99,6 +106,7 @@ export const sites = pgTable(
|
|||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
subnet: varchar("subnet"),
|
subnet: varchar("subnet"),
|
||||||
|
exitNodeSubnet: text("exitNodeSubnet"), // this is the subnet when connecting to an exit node and INCLUDES THE CIDR
|
||||||
megabytesIn: real("bytesIn").default(0),
|
megabytesIn: real("bytesIn").default(0),
|
||||||
megabytesOut: real("bytesOut").default(0),
|
megabytesOut: real("bytesOut").default(0),
|
||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||||
@@ -194,7 +202,12 @@ export const resources = pgTable(
|
|||||||
postAuthPath: text("postAuthPath"),
|
postAuthPath: text("postAuthPath"),
|
||||||
health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
||||||
wildcard: boolean("wildcard").notNull().default(false),
|
wildcard: boolean("wildcard").notNull().default(false),
|
||||||
mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
|
mode: text("mode")
|
||||||
|
.default("http")
|
||||||
|
.$type<
|
||||||
|
"rdp" | "ssh" | "http" | "vnc" | "inference" | "tcp" | "udp"
|
||||||
|
>()
|
||||||
|
.notNull(),
|
||||||
pamMode: varchar("pamMode", { length: 32 })
|
pamMode: varchar("pamMode", { length: 32 })
|
||||||
.$type<"passthrough" | "push">()
|
.$type<"passthrough" | "push">()
|
||||||
.default("passthrough"),
|
.default("passthrough"),
|
||||||
@@ -215,6 +228,41 @@ export const resources = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const resourceAiProviders = pgTable(
|
||||||
|
"resourceAiProviders",
|
||||||
|
{
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
providerId: integer("providerId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
|
||||||
|
accessMode: varchar("accessMode")
|
||||||
|
.$type<"inherit" | "select">()
|
||||||
|
.notNull()
|
||||||
|
.default("inherit"),
|
||||||
|
enabled: boolean("enabled").notNull().default(true)
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.resourceId, t.providerId] })]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const resourceAiModels = pgTable(
|
||||||
|
"resourceAiModels",
|
||||||
|
{
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
modelId: integer("modelId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiModels.modelId, { onDelete: "cascade" }),
|
||||||
|
listType: varchar("listType")
|
||||||
|
.$type<"allow" | "block">()
|
||||||
|
.notNull()
|
||||||
|
.default("allow")
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.resourceId, t.modelId] })]
|
||||||
|
);
|
||||||
|
|
||||||
export const labels = pgTable("labels", {
|
export const labels = pgTable("labels", {
|
||||||
labelId: serial("labelId").primaryKey(),
|
labelId: serial("labelId").primaryKey(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
@@ -317,11 +365,18 @@ export const targets = pgTable(
|
|||||||
"targets",
|
"targets",
|
||||||
{
|
{
|
||||||
targetId: serial("targetId").primaryKey(),
|
targetId: serial("targetId").primaryKey(),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId").references(
|
||||||
.references(() => resources.resourceId, {
|
() => resources.resourceId,
|
||||||
|
{
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
}
|
||||||
.notNull(),
|
),
|
||||||
|
providerId: integer("providerId").references(
|
||||||
|
() => aiProviders.providerId,
|
||||||
|
{
|
||||||
|
onDelete: "cascade"
|
||||||
|
}
|
||||||
|
),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.references(() => sites.siteId, {
|
.references(() => sites.siteId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -345,6 +400,7 @@ export const targets = pgTable(
|
|||||||
},
|
},
|
||||||
(t) => [
|
(t) => [
|
||||||
index("idx_targets_resourceid_siteid").on(t.resourceId, t.siteId),
|
index("idx_targets_resourceid_siteid").on(t.resourceId, t.siteId),
|
||||||
|
index("idx_targets_providerid_siteid").on(t.providerId, t.siteId),
|
||||||
index("idx_targets_site_enabled_priority_target_resource")
|
index("idx_targets_site_enabled_priority_target_resource")
|
||||||
.on(t.siteId, t.priority.desc(), t.targetId, t.resourceId)
|
.on(t.siteId, t.priority.desc(), t.targetId, t.resourceId)
|
||||||
.where(sql`${t.enabled} = true`)
|
.where(sql`${t.enabled} = true`)
|
||||||
@@ -424,11 +480,14 @@ export const siteResources = pgTable(
|
|||||||
onDelete: "restrict"
|
onDelete: "restrict"
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
requiresExitNodeConnection: boolean("requiresExitNodeConnection")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
niceId: varchar("niceId").notNull(),
|
niceId: varchar("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
ssl: boolean("ssl").notNull().default(false),
|
ssl: boolean("ssl").notNull().default(false),
|
||||||
mode: varchar("mode")
|
mode: varchar("mode")
|
||||||
.$type<"host" | "cidr" | "http" | "ssh">()
|
.$type<"host" | "cidr" | "http" | "ssh" | "inference">()
|
||||||
.notNull(), // "host" | "cidr" | "http"
|
.notNull(), // "host" | "cidr" | "http"
|
||||||
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
@@ -463,6 +522,45 @@ export const siteResources = pgTable(
|
|||||||
(t) => [index("idx_siteresources_orgid_niceid").on(t.orgId, t.niceId)]
|
(t) => [index("idx_siteresources_orgid_niceid").on(t.orgId, t.niceId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const siteResourceAiProviders = pgTable(
|
||||||
|
"siteResourceAiProviders",
|
||||||
|
{
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
providerId: integer("providerId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
|
||||||
|
accessMode: varchar("accessMode")
|
||||||
|
.$type<"inherit" | "select">()
|
||||||
|
.notNull()
|
||||||
|
.default("inherit"),
|
||||||
|
enabled: boolean("enabled").notNull().default(true)
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.siteResourceId, t.providerId] })]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const siteResourceAiModels = pgTable(
|
||||||
|
"siteResourceAiModels",
|
||||||
|
{
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
modelId: integer("modelId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiModels.modelId, { onDelete: "cascade" }),
|
||||||
|
listType: varchar("listType")
|
||||||
|
.$type<"allow" | "block">()
|
||||||
|
.notNull()
|
||||||
|
.default("allow")
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.siteResourceId, t.modelId] })]
|
||||||
|
);
|
||||||
|
|
||||||
export const networks = pgTable(
|
export const networks = pgTable(
|
||||||
"networks",
|
"networks",
|
||||||
{
|
{
|
||||||
@@ -665,7 +763,7 @@ export const roles = pgTable("roles", {
|
|||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
description: varchar("description"),
|
description: varchar("description"),
|
||||||
requireDeviceApproval: boolean("requireDeviceApproval").default(false),
|
requireDeviceApproval: boolean("requireDeviceApproval").default(false),
|
||||||
sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("none"), // "none" | "full" | "commands"
|
sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("full"), // "none" | "full" | "commands"
|
||||||
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
||||||
sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
|
sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
|
||||||
sshUnixGroups: text("sshUnixGroups").default("[]")
|
sshUnixGroups: text("sshUnixGroups").default("[]")
|
||||||
@@ -1150,6 +1248,52 @@ export const apiKeyOrg = pgTable("apiKeyOrg", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const virtualApiKeys = pgTable(
|
||||||
|
"virtualApiKeys",
|
||||||
|
{
|
||||||
|
virtualApiKeyId: varchar("virtualApiKeyId").primaryKey(),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
kind: varchar("kind").$type<"user" | "manual">().notNull(),
|
||||||
|
userId: varchar("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
name: varchar("name"),
|
||||||
|
description: varchar("description"),
|
||||||
|
token: varchar("token").notNull(),
|
||||||
|
lastChars: varchar("lastChars").notNull(),
|
||||||
|
allResources: boolean("allResources").notNull().default(false),
|
||||||
|
expiresAt: bigint("expiresAt", { mode: "number" }),
|
||||||
|
lastUsedAt: bigint("lastUsedAt", { mode: "number" }),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
|
createdByUserId: varchar("createdByUserId").references(
|
||||||
|
() => users.userId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
uniqueIndex("virtual_api_key_user_identity_uniq")
|
||||||
|
.on(t.orgId, t.userId)
|
||||||
|
.where(sql`${t.kind} = 'user'`)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const virtualApiKeyResources = pgTable(
|
||||||
|
"virtualApiKeyResources",
|
||||||
|
{
|
||||||
|
virtualApiKeyId: varchar("virtualApiKeyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => virtualApiKeys.virtualApiKeyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.virtualApiKeyId, t.resourceId] })]
|
||||||
|
);
|
||||||
|
|
||||||
export const idpOrg = pgTable("idpOrg", {
|
export const idpOrg = pgTable("idpOrg", {
|
||||||
idpId: integer("idpId")
|
idpId: integer("idpId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -1181,7 +1325,7 @@ export const clients = pgTable(
|
|||||||
olmId: text("olmId"), // to lock it to a specific olm optionally
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
subnet: varchar("subnet").notNull(),
|
exitNodeSubnet: varchar("exitNodeSubnet").notNull(), // INCLUDES THE CIDR
|
||||||
megabytesIn: real("bytesIn"),
|
megabytesIn: real("bytesIn"),
|
||||||
megabytesOut: real("bytesOut"),
|
megabytesOut: real("bytesOut"),
|
||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||||
@@ -1540,6 +1684,358 @@ export const statusHistory = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const aiProviders = pgTable(
|
||||||
|
"aiProviders",
|
||||||
|
{
|
||||||
|
providerId: serial("providerId").primaryKey(),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
name: varchar("name").notNull(),
|
||||||
|
niceId: varchar("niceId").notNull(),
|
||||||
|
type: varchar("type")
|
||||||
|
.$type<
|
||||||
|
| "openai"
|
||||||
|
| "anthropic"
|
||||||
|
| "googleGemini"
|
||||||
|
| "vertexAi"
|
||||||
|
| "bedrock"
|
||||||
|
| "microsoftFoundry"
|
||||||
|
| "openRouter"
|
||||||
|
| "vercelAiGateway"
|
||||||
|
| "custom"
|
||||||
|
>()
|
||||||
|
.notNull(),
|
||||||
|
upstreamUrl: text("upstreamUrl"),
|
||||||
|
apiKey: text("apiKey"),
|
||||||
|
apiKeyLastChars: varchar("apiKeyLastChars"),
|
||||||
|
authType: varchar("authType")
|
||||||
|
.$type<
|
||||||
|
| "bearer"
|
||||||
|
| "x-api-key"
|
||||||
|
| "x-goog-api-key"
|
||||||
|
| "hec"
|
||||||
|
| "cf-aig-authorization"
|
||||||
|
| "none"
|
||||||
|
| "passthrough"
|
||||||
|
>()
|
||||||
|
.notNull(),
|
||||||
|
routingMode: varchar("routingMode")
|
||||||
|
.$type<"url" | "target">()
|
||||||
|
.notNull()
|
||||||
|
.default("url"),
|
||||||
|
capabilities: text("capabilities").notNull().default("[]"),
|
||||||
|
headers: text("headers"), // JSON array of { name, value }
|
||||||
|
skipTlsVerification: boolean("skipTlsVerification")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
|
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||||
|
},
|
||||||
|
(t) => [index("idx_aiProviders_orgId_niceId").on(t.orgId, t.niceId)]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const aiModels = pgTable(
|
||||||
|
"aiModels",
|
||||||
|
{
|
||||||
|
modelId: serial("modelId").primaryKey(),
|
||||||
|
providerId: integer("providerId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
|
||||||
|
modelKey: varchar("modelKey").notNull(),
|
||||||
|
name: varchar("name").notNull(),
|
||||||
|
listType: varchar("listType")
|
||||||
|
.$type<"allow" | "block">()
|
||||||
|
.notNull()
|
||||||
|
.default("allow"),
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
|
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||||
|
},
|
||||||
|
(t) => [unique("ai_model_provider_key_uniq").on(t.providerId, t.modelKey)]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const aiBudgets = pgTable(
|
||||||
|
"aiBudgets",
|
||||||
|
{
|
||||||
|
budgetId: serial("budgetId").primaryKey(),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
providerId: integer("providerId").references(
|
||||||
|
() => aiProviders.providerId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
modelId: integer("modelId").references(() => aiModels.modelId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
resourceId: integer("resourceId").references(
|
||||||
|
() => resources.resourceId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
roleId: integer("roleId").references(() => roles.roleId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
virtualApiKeyId: varchar("virtualApiKeyId").references(
|
||||||
|
() => virtualApiKeys.virtualApiKeyId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
amount: real("amount").notNull(),
|
||||||
|
unit: varchar("unit").$type<"usd" | "tokens">().notNull(),
|
||||||
|
period: varchar("period")
|
||||||
|
.$type<
|
||||||
|
| "monthly"
|
||||||
|
| "yearly"
|
||||||
|
| "lifetime"
|
||||||
|
| "daily"
|
||||||
|
| "hourly"
|
||||||
|
| "weekly"
|
||||||
|
>()
|
||||||
|
.notNull()
|
||||||
|
.default("monthly"),
|
||||||
|
enforcement: varchar("enforcement")
|
||||||
|
.$type<"hard" | "soft">()
|
||||||
|
.notNull()
|
||||||
|
.default("hard"),
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
|
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
unique("ai_budget_provider_uniq").on(t.providerId, t.unit, t.period),
|
||||||
|
unique("ai_budget_model_uniq").on(t.modelId, t.unit, t.period),
|
||||||
|
unique("ai_budget_resource_uniq").on(t.resourceId, t.unit, t.period),
|
||||||
|
unique("ai_budget_site_resource_uniq").on(
|
||||||
|
t.siteResourceId,
|
||||||
|
t.unit,
|
||||||
|
t.period
|
||||||
|
),
|
||||||
|
unique("ai_budget_role_uniq").on(t.roleId, t.unit, t.period),
|
||||||
|
unique("ai_budget_virtual_api_key_uniq").on(
|
||||||
|
t.virtualApiKeyId,
|
||||||
|
t.unit,
|
||||||
|
t.period
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const aiUsageRecords = pgTable(
|
||||||
|
"aiUsageRecords",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
providerId: integer("providerId").references(
|
||||||
|
() => aiProviders.providerId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
resourceId: integer("resourceId").references(
|
||||||
|
() => resources.resourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
userId: varchar("userId").references(() => users.userId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
virtualApiKeyId: varchar("virtualApiKeyId").references(
|
||||||
|
() => virtualApiKeys.virtualApiKeyId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
// Links this usage record back to the aiSessionLog row for the same
|
||||||
|
// request (aiSessionLog.sessionId), so token/cost usage can be shown
|
||||||
|
// alongside the session transcript. Not a DB-level FK - aiSessionLog
|
||||||
|
// lives in the separate logs database. Nullable because the session
|
||||||
|
// log may be disabled (retention set to 0) while usage tracking
|
||||||
|
// stays on.
|
||||||
|
sessionId: varchar("sessionId"),
|
||||||
|
requestedModel: varchar("requestedModel").notNull(),
|
||||||
|
promptTokens: integer("promptTokens").notNull().default(0),
|
||||||
|
cacheReadTokens: integer("cacheReadTokens").notNull().default(0),
|
||||||
|
cacheWriteTokens: integer("cacheWriteTokens").notNull().default(0),
|
||||||
|
completionTokens: integer("completionTokens").notNull().default(0),
|
||||||
|
reasoningTokens: integer("reasoningTokens").notNull().default(0),
|
||||||
|
totalTokens: integer("totalTokens").notNull().default(0),
|
||||||
|
costUsd: real("costUsd"),
|
||||||
|
estimated: boolean("estimated").notNull().default(false),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_ai_usage_records_org_provider_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.providerId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_org_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.resourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_org_site_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.siteResourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_org_user_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.userId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_org_virtual_api_key_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.virtualApiKeyId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_session").on(t.sessionId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const aiBudgetBreachEvents = pgTable(
|
||||||
|
"aiBudgetBreachEvents",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
budgetId: integer("budgetId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiBudgets.budgetId, { onDelete: "cascade" }),
|
||||||
|
enforcement: varchar("enforcement").$type<"hard" | "soft">().notNull(),
|
||||||
|
unit: varchar("unit").$type<"usd" | "tokens">().notNull(),
|
||||||
|
period: varchar("period")
|
||||||
|
.$type<
|
||||||
|
| "monthly"
|
||||||
|
| "yearly"
|
||||||
|
| "lifetime"
|
||||||
|
| "daily"
|
||||||
|
| "hourly"
|
||||||
|
| "weekly"
|
||||||
|
>()
|
||||||
|
.notNull(),
|
||||||
|
amount: real("amount").notNull(),
|
||||||
|
usageAmount: real("usageAmount").notNull(),
|
||||||
|
blocked: boolean("blocked").notNull(),
|
||||||
|
requestUserId: varchar("requestUserId").references(() => users.userId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_ai_budget_breach_events_budget_created").on(
|
||||||
|
t.budgetId,
|
||||||
|
t.createdAt
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Logs the aggregated prompt + response for a single AI gateway request, for
|
||||||
|
// session replay. One row per request (not per streaming chunk). `sessionId`
|
||||||
|
// is a fresh random id per row for now - no cross-request correlation yet,
|
||||||
|
// but the column exists so a future pass can link multiple rows into a real
|
||||||
|
// multi-turn session.
|
||||||
|
export const aiSessionLog = pgTable(
|
||||||
|
"aiSessionLog",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
sessionId: varchar("sessionId").notNull(),
|
||||||
|
orgId: varchar("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
providerId: integer("providerId").references(
|
||||||
|
() => aiProviders.providerId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
capability: varchar("capability").notNull(),
|
||||||
|
resourceId: integer("resourceId").references(
|
||||||
|
() => resources.resourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
userId: varchar("userId").references(() => users.userId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
virtualApiKeyId: varchar("virtualApiKeyId").references(
|
||||||
|
() => virtualApiKeys.virtualApiKeyId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
requestedModel: varchar("requestedModel"),
|
||||||
|
isStream: boolean("isStream").notNull().default(false),
|
||||||
|
requestBody: text("requestBody"),
|
||||||
|
responseBody: text("responseBody"),
|
||||||
|
// Capability-agnostic message transcript (JSON-encoded
|
||||||
|
// NormalizedAiMessage[] from server/lib/aiMessageNormalization.ts),
|
||||||
|
// computed at write time so search/display never need per-capability
|
||||||
|
// parsing logic. Null when normalization couldn't recognize the
|
||||||
|
// shape - callers fall back to requestBody/responseBody.
|
||||||
|
normalizedRequest: text("normalizedRequest"),
|
||||||
|
normalizedResponse: text("normalizedResponse"),
|
||||||
|
// True if any of the request/response (raw or normalized) fields
|
||||||
|
// were cut short at AI_SESSION_LOG_MAX_BODY_CHARS before storage.
|
||||||
|
truncated: boolean("truncated").notNull().default(false),
|
||||||
|
statusCode: integer("statusCode"),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull() // epoch ms
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt),
|
||||||
|
index("idx_ai_session_log_org_provider_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.providerId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.resourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_site_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.siteResourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_user_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.userId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_virtual_api_key_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.virtualApiKeyId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_session").on(t.sessionId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const certificates = pgTable("certificates", {
|
||||||
|
certId: serial("certId").primaryKey(),
|
||||||
|
domain: varchar("domain", { length: 255 }).notNull().unique(),
|
||||||
|
domainId: varchar("domainId").references(() => domains.domainId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
wildcard: boolean("wildcard").default(false),
|
||||||
|
status: varchar("status", { length: 50 }).notNull().default("pending"), // pending, requested, valid, expired, failed
|
||||||
|
expiresAt: bigint("expiresAt", { mode: "number" }),
|
||||||
|
lastRenewalAttempt: bigint("lastRenewalAttempt", { mode: "number" }),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
|
updatedAt: bigint("updatedAt", { mode: "number" }).notNull(),
|
||||||
|
orderId: varchar("orderId", { length: 500 }),
|
||||||
|
errorMessage: text("errorMessage"),
|
||||||
|
renewalCount: integer("renewalCount").default(0),
|
||||||
|
certFile: text("certFile"),
|
||||||
|
keyFile: text("keyFile")
|
||||||
|
});
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -1595,6 +2091,10 @@ export type Idp = InferSelectModel<typeof idp>;
|
|||||||
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||||
|
export type VirtualApiKey = InferSelectModel<typeof virtualApiKeys>;
|
||||||
|
export type VirtualApiKeyResource = InferSelectModel<
|
||||||
|
typeof virtualApiKeyResources
|
||||||
|
>;
|
||||||
export type Client = InferSelectModel<typeof clients>;
|
export type Client = InferSelectModel<typeof clients>;
|
||||||
export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
|
export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
|
||||||
export type Olm = InferSelectModel<typeof olms>;
|
export type Olm = InferSelectModel<typeof olms>;
|
||||||
@@ -1624,3 +2124,16 @@ export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
|
|||||||
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
||||||
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
||||||
export type ResourcePolicyRule = InferSelectModel<typeof resourcePolicyRules>;
|
export type ResourcePolicyRule = InferSelectModel<typeof resourcePolicyRules>;
|
||||||
|
export type AiProvider = InferSelectModel<typeof aiProviders>;
|
||||||
|
export type AiModel = InferSelectModel<typeof aiModels>;
|
||||||
|
export type AiBudget = InferSelectModel<typeof aiBudgets>;
|
||||||
|
export type AiUsageRecord = InferSelectModel<typeof aiUsageRecords>;
|
||||||
|
export type AiBudgetBreachEvent = InferSelectModel<typeof aiBudgetBreachEvents>;
|
||||||
|
export type AiSessionLog = InferSelectModel<typeof aiSessionLog>;
|
||||||
|
export type ResourceAiProvider = InferSelectModel<typeof resourceAiProviders>;
|
||||||
|
export type SiteResourceAiProvider = InferSelectModel<
|
||||||
|
typeof siteResourceAiProviders
|
||||||
|
>;
|
||||||
|
export type ResourceAiModel = InferSelectModel<typeof resourceAiModels>;
|
||||||
|
export type SiteResourceAiModel = InferSelectModel<typeof siteResourceAiModels>;
|
||||||
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ import {
|
|||||||
resourcePolicyPassword,
|
resourcePolicyPassword,
|
||||||
ResourcePolicyPassword,
|
ResourcePolicyPassword,
|
||||||
resourcePolicyHeaderAuth,
|
resourcePolicyHeaderAuth,
|
||||||
ResourcePolicyHeaderAuth
|
ResourcePolicyHeaderAuth,
|
||||||
|
resourceWhitelist,
|
||||||
|
resourcePolicyWhiteList
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { alias } from "@server/db";
|
import { alias } from "@server/db";
|
||||||
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
||||||
@@ -448,6 +450,36 @@ export async function getResourceRules(
|
|||||||
return [...directRules, ...offsetPolicyRules] as ResourceRule[];
|
return [...directRules, ...offsetPolicyRules] as ResourceRule[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the whitelisted email associated with a resource session's whitelist
|
||||||
|
* match (either a direct resource whitelist entry or a resource policy
|
||||||
|
* whitelist entry).
|
||||||
|
*/
|
||||||
|
export async function getWhitelistEmail(
|
||||||
|
whitelistId?: number | null,
|
||||||
|
policyWhitelistId?: number | null
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (whitelistId) {
|
||||||
|
const [row] = await db
|
||||||
|
.select({ email: resourceWhitelist.email })
|
||||||
|
.from(resourceWhitelist)
|
||||||
|
.where(eq(resourceWhitelist.whitelistId, whitelistId))
|
||||||
|
.limit(1);
|
||||||
|
return row?.email ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (policyWhitelistId) {
|
||||||
|
const [row] = await db
|
||||||
|
.select({ email: resourcePolicyWhiteList.email })
|
||||||
|
.from(resourcePolicyWhiteList)
|
||||||
|
.where(eq(resourcePolicyWhiteList.whitelistId, policyWhitelistId))
|
||||||
|
.limit(1);
|
||||||
|
return row?.email ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get organization login page
|
* Get organization login page
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { APP_PATH } from "@server/lib/consts";
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
import { existsSync, mkdirSync } from "fs";
|
import { existsSync, mkdirSync } from "fs";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export const location = path.join(APP_PATH, "db", "db.sqlite");
|
export const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
export const exists = checkFileExists(location);
|
export const exists = checkFileExists(location);
|
||||||
@@ -12,7 +13,11 @@ export const exists = checkFileExists(location);
|
|||||||
bootstrapVolume();
|
bootstrapVolume();
|
||||||
|
|
||||||
function createDb() {
|
function createDb() {
|
||||||
const sqlite = new Database(location);
|
const verbose =
|
||||||
|
process.env.QUERY_LOGGING == "true"
|
||||||
|
? (message: unknown) => logger.debug(String(message))
|
||||||
|
: undefined;
|
||||||
|
const sqlite = new Database(location, { verbose });
|
||||||
|
|
||||||
if (process.env.ENABLE_SQLITE_WAL_MODE == "true") {
|
if (process.env.ENABLE_SQLITE_WAL_MODE == "true") {
|
||||||
// Enable WAL mode — allows concurrent readers + single writer, preventing
|
// Enable WAL mode — allows concurrent readers + single writer, preventing
|
||||||
|
|||||||
@@ -23,25 +23,6 @@ import {
|
|||||||
users
|
users
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
|
|
||||||
export const certificates = sqliteTable("certificates", {
|
|
||||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
|
||||||
domain: text("domain").notNull().unique(),
|
|
||||||
domainId: text("domainId").references(() => domains.domainId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
}),
|
|
||||||
wildcard: integer("wildcard", { mode: "boolean" }).default(false),
|
|
||||||
status: text("status").notNull().default("pending"), // pending, requested, valid, expired, failed
|
|
||||||
expiresAt: integer("expiresAt"),
|
|
||||||
lastRenewalAttempt: integer("lastRenewalAttempt"),
|
|
||||||
createdAt: integer("createdAt").notNull(),
|
|
||||||
updatedAt: integer("updatedAt").notNull(),
|
|
||||||
orderId: text("orderId"),
|
|
||||||
errorMessage: text("errorMessage"),
|
|
||||||
renewalCount: integer("renewalCount").default(0),
|
|
||||||
certFile: text("certFile"),
|
|
||||||
keyFile: text("keyFile")
|
|
||||||
});
|
|
||||||
|
|
||||||
export const dnsChallenge = sqliteTable("dnsChallenges", {
|
export const dnsChallenge = sqliteTable("dnsChallenges", {
|
||||||
dnsChallengeId: integer("dnsChallengeId").primaryKey({
|
dnsChallengeId: integer("dnsChallengeId").primaryKey({
|
||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
@@ -89,7 +70,8 @@ export const subscriptions = sqliteTable("subscriptions", {
|
|||||||
expiresAt: integer("expiresAt"),
|
expiresAt: integer("expiresAt"),
|
||||||
trial: integer("trial", { mode: "boolean" }).default(false),
|
trial: integer("trial", { mode: "boolean" }).default(false),
|
||||||
billingCycleAnchor: integer("billingCycleAnchor"),
|
billingCycleAnchor: integer("billingCycleAnchor"),
|
||||||
type: text("type") // tier1, tier2, tier3, or license
|
type: text("type"), // tier1, tier2, tier3, or license
|
||||||
|
override: integer("override", { mode: "boolean" }).default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const subscriptionItems = sqliteTable("subscriptionItems", {
|
export const subscriptionItems = sqliteTable("subscriptionItems", {
|
||||||
@@ -627,7 +609,6 @@ export const trialNotifications = sqliteTable("trialNotifications", {
|
|||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
|
||||||
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
||||||
export type Customer = InferSelectModel<typeof customers>;
|
export type Customer = InferSelectModel<typeof customers>;
|
||||||
export type Subscription = InferSelectModel<typeof subscriptions>;
|
export type Subscription = InferSelectModel<typeof subscriptions>;
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
|
check,
|
||||||
index,
|
index,
|
||||||
integer,
|
integer,
|
||||||
primaryKey,
|
primaryKey,
|
||||||
|
real,
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
text,
|
text,
|
||||||
unique
|
unique,
|
||||||
|
uniqueIndex
|
||||||
} from "drizzle-orm/sqlite-core";
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
export const domains = sqliteTable("domains", {
|
export const domains = sqliteTable("domains", {
|
||||||
@@ -61,6 +64,11 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
|
settingsLogRetentionDaysAISessions: integer(
|
||||||
|
"settingsLogRetentionDaysAISessions"
|
||||||
|
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
|
.notNull()
|
||||||
|
.default(7),
|
||||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||||
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
|
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
|
||||||
@@ -107,7 +115,7 @@ export const sites = sqliteTable("sites", {
|
|||||||
}),
|
}),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
pubKey: text("pubKey"),
|
pubKey: text("pubKey"),
|
||||||
subnet: text("subnet"),
|
exitNodeSubnet: text("exitNodeSubnet"),
|
||||||
megabytesIn: integer("bytesIn").default(0),
|
megabytesIn: integer("bytesIn").default(0),
|
||||||
megabytesOut: integer("bytesOut").default(0),
|
megabytesOut: integer("bytesOut").default(0),
|
||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
@@ -203,7 +211,10 @@ export const resources = sqliteTable("resources", {
|
|||||||
postAuthPath: text("postAuthPath"),
|
postAuthPath: text("postAuthPath"),
|
||||||
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
||||||
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false),
|
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false),
|
||||||
mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
|
mode: text("mode")
|
||||||
|
.default("http")
|
||||||
|
.$type<"rdp" | "ssh" | "http" | "vnc" | "inference" | "tcp" | "udp">()
|
||||||
|
.notNull(), // rdp, ssh, http, vnc, inference
|
||||||
pamMode: text("pamMode")
|
pamMode: text("pamMode")
|
||||||
.$type<"passthrough" | "push">()
|
.$type<"passthrough" | "push">()
|
||||||
.default("passthrough"),
|
.default("passthrough"),
|
||||||
@@ -214,6 +225,41 @@ export const resources = sqliteTable("resources", {
|
|||||||
status: text("status").$type<"pending" | "approved">().default("approved")
|
status: text("status").$type<"pending" | "approved">().default("approved")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourceAiProviders = sqliteTable(
|
||||||
|
"resourceAiProviders",
|
||||||
|
{
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
providerId: integer("providerId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
|
||||||
|
accessMode: text("accessMode")
|
||||||
|
.$type<"inherit" | "select">()
|
||||||
|
.notNull()
|
||||||
|
.default("inherit"),
|
||||||
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.resourceId, t.providerId] })]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const resourceAiModels = sqliteTable(
|
||||||
|
"resourceAiModels",
|
||||||
|
{
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
modelId: integer("modelId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiModels.modelId, { onDelete: "cascade" }),
|
||||||
|
listType: text("listType")
|
||||||
|
.$type<"allow" | "block">()
|
||||||
|
.notNull()
|
||||||
|
.default("allow")
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.resourceId, t.modelId] })]
|
||||||
|
);
|
||||||
|
|
||||||
export const labels = sqliteTable("labels", {
|
export const labels = sqliteTable("labels", {
|
||||||
labelId: integer("labelId").primaryKey({ autoIncrement: true }),
|
labelId: integer("labelId").primaryKey({ autoIncrement: true }),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
@@ -322,11 +368,12 @@ export const clientLabels = sqliteTable(
|
|||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
targetId: integer("targetId").primaryKey({ autoIncrement: true }),
|
targetId: integer("targetId").primaryKey({ autoIncrement: true }),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId").references(() => resources.resourceId, {
|
||||||
.references(() => resources.resourceId, {
|
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
}),
|
||||||
.notNull(),
|
providerId: integer("providerId").references(() => aiProviders.providerId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.references(() => sites.siteId, {
|
.references(() => sites.siteId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -422,10 +469,17 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
() => networks.networkId,
|
() => networks.networkId,
|
||||||
{ onDelete: "restrict" }
|
{ onDelete: "restrict" }
|
||||||
),
|
),
|
||||||
|
requiresExitNodeConnection: integer("requiresExitNodeConnection", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
niceId: text("niceId").notNull(),
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
||||||
mode: text("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http"
|
mode: text("mode")
|
||||||
|
.$type<"host" | "cidr" | "http" | "ssh" | "inference">()
|
||||||
|
.notNull(), // "host" | "cidr" | "http"
|
||||||
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
@@ -453,6 +507,45 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
status: text("status").$type<"pending" | "approved">().default("approved")
|
status: text("status").$type<"pending" | "approved">().default("approved")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const siteResourceAiProviders = sqliteTable(
|
||||||
|
"siteResourceAiProviders",
|
||||||
|
{
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
providerId: integer("providerId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
|
||||||
|
accessMode: text("accessMode")
|
||||||
|
.$type<"inherit" | "select">()
|
||||||
|
.notNull()
|
||||||
|
.default("inherit"),
|
||||||
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.siteResourceId, t.providerId] })]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const siteResourceAiModels = sqliteTable(
|
||||||
|
"siteResourceAiModels",
|
||||||
|
{
|
||||||
|
siteResourceId: integer("siteResourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => siteResources.siteResourceId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
modelId: integer("modelId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiModels.modelId, { onDelete: "cascade" }),
|
||||||
|
listType: text("listType")
|
||||||
|
.$type<"allow" | "block">()
|
||||||
|
.notNull()
|
||||||
|
.default("allow")
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.siteResourceId, t.modelId] })]
|
||||||
|
);
|
||||||
|
|
||||||
export const networks = sqliteTable("networks", {
|
export const networks = sqliteTable("networks", {
|
||||||
networkId: integer("networkId").primaryKey({ autoIncrement: true }),
|
networkId: integer("networkId").primaryKey({ autoIncrement: true }),
|
||||||
niceId: text("niceId"),
|
niceId: text("niceId"),
|
||||||
@@ -599,6 +692,7 @@ export const clients = sqliteTable("clients", {
|
|||||||
pubKey: text("pubKey"),
|
pubKey: text("pubKey"),
|
||||||
olmId: text("olmId"), // to lock it to a specific olm optionally
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
subnet: text("subnet").notNull(),
|
subnet: text("subnet").notNull(),
|
||||||
|
exitNodeSubnet: text("exitNodeSubnet"), // this is the subnet when connecting to an exit node
|
||||||
megabytesIn: integer("bytesIn"),
|
megabytesIn: integer("bytesIn"),
|
||||||
megabytesOut: integer("bytesOut"),
|
megabytesOut: integer("bytesOut"),
|
||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
@@ -901,7 +995,7 @@ export const roles = sqliteTable("roles", {
|
|||||||
requireDeviceApproval: integer("requireDeviceApproval", {
|
requireDeviceApproval: integer("requireDeviceApproval", {
|
||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
}).default(false),
|
}).default(false),
|
||||||
sshSudoMode: text("sshSudoMode").default("none"), // "none" | "full" | "commands"
|
sshSudoMode: text("sshSudoMode").default("full"), // "none" | "full" | "commands"
|
||||||
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
||||||
sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default(
|
sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default(
|
||||||
true
|
true
|
||||||
@@ -1411,6 +1505,54 @@ export const apiKeyOrg = sqliteTable("apiKeyOrg", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const virtualApiKeys = sqliteTable(
|
||||||
|
"virtualApiKeys",
|
||||||
|
{
|
||||||
|
virtualApiKeyId: text("virtualApiKeyId").primaryKey(),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
kind: text("kind").$type<"user" | "manual">().notNull(),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
name: text("name"),
|
||||||
|
description: text("description"),
|
||||||
|
token: text("token").notNull(),
|
||||||
|
lastChars: text("lastChars").notNull(),
|
||||||
|
allResources: integer("allResources", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
expiresAt: integer("expiresAt"),
|
||||||
|
lastUsedAt: integer("lastUsedAt"),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
createdByUserId: text("createdByUserId").references(
|
||||||
|
() => users.userId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
uniqueIndex("virtual_api_key_user_identity_uniq")
|
||||||
|
.on(t.orgId, t.userId)
|
||||||
|
.where(sql`${t.kind} = 'user'`)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const virtualApiKeyResources = sqliteTable(
|
||||||
|
"virtualApiKeyResources",
|
||||||
|
{
|
||||||
|
virtualApiKeyId: text("virtualApiKeyId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => virtualApiKeys.virtualApiKeyId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||||
|
},
|
||||||
|
(t) => [primaryKey({ columns: [t.virtualApiKeyId, t.resourceId] })]
|
||||||
|
);
|
||||||
|
|
||||||
export const idpOrg = sqliteTable("idpOrg", {
|
export const idpOrg = sqliteTable("idpOrg", {
|
||||||
idpId: integer("idpId")
|
idpId: integer("idpId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -1526,6 +1668,370 @@ export const statusHistory = sqliteTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const aiProviders = sqliteTable(
|
||||||
|
"aiProviders",
|
||||||
|
{
|
||||||
|
providerId: integer("providerId").primaryKey({ autoIncrement: true }),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
niceId: text("niceId").notNull(),
|
||||||
|
type: text("type")
|
||||||
|
.$type<
|
||||||
|
| "openai"
|
||||||
|
| "anthropic"
|
||||||
|
| "googleGemini"
|
||||||
|
| "vertexAi"
|
||||||
|
| "bedrock"
|
||||||
|
| "microsoftFoundry"
|
||||||
|
| "openRouter"
|
||||||
|
| "vercelAiGateway"
|
||||||
|
| "custom"
|
||||||
|
>()
|
||||||
|
.notNull(),
|
||||||
|
upstreamUrl: text("upstreamUrl"),
|
||||||
|
apiKey: text("apiKey"),
|
||||||
|
apiKeyLastChars: text("apiKeyLastChars"),
|
||||||
|
authType: text("authType")
|
||||||
|
.$type<
|
||||||
|
| "bearer"
|
||||||
|
| "x-api-key"
|
||||||
|
| "x-goog-api-key"
|
||||||
|
| "hec"
|
||||||
|
| "cf-aig-authorization"
|
||||||
|
| "none"
|
||||||
|
| "passthrough"
|
||||||
|
>()
|
||||||
|
.notNull(),
|
||||||
|
routingMode: text("routingMode")
|
||||||
|
.$type<"url" | "target">()
|
||||||
|
.notNull()
|
||||||
|
.default("url"),
|
||||||
|
capabilities: text("capabilities").notNull().default("[]"),
|
||||||
|
headers: text("headers"), // JSON array of { name, value }
|
||||||
|
skipTlsVerification: integer("skipTlsVerification", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
enabled: integer("enabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(true),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
updatedAt: integer("updatedAt").notNull()
|
||||||
|
},
|
||||||
|
(t) => [index("idx_aiProviders_orgId_niceId").on(t.orgId, t.niceId)]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const aiModels = sqliteTable(
|
||||||
|
"aiModels",
|
||||||
|
{
|
||||||
|
modelId: integer("modelId").primaryKey({ autoIncrement: true }),
|
||||||
|
providerId: integer("providerId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
|
||||||
|
modelKey: text("modelKey").notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
listType: text("listType")
|
||||||
|
.$type<"allow" | "block">()
|
||||||
|
.notNull()
|
||||||
|
.default("allow"),
|
||||||
|
enabled: integer("enabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(true),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
updatedAt: integer("updatedAt").notNull()
|
||||||
|
},
|
||||||
|
(t) => [unique("ai_model_provider_key_uniq").on(t.providerId, t.modelKey)]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const aiBudgets = sqliteTable(
|
||||||
|
"aiBudgets",
|
||||||
|
{
|
||||||
|
budgetId: integer("budgetId").primaryKey({ autoIncrement: true }),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
providerId: integer("providerId").references(
|
||||||
|
() => aiProviders.providerId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
modelId: integer("modelId").references(() => aiModels.modelId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
resourceId: integer("resourceId").references(
|
||||||
|
() => resources.resourceId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
roleId: integer("roleId").references(() => roles.roleId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
virtualApiKeyId: text("virtualApiKeyId").references(
|
||||||
|
() => virtualApiKeys.virtualApiKeyId,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
amount: real("amount").notNull(),
|
||||||
|
unit: text("unit").$type<"usd" | "tokens">().notNull(),
|
||||||
|
period: text("period")
|
||||||
|
.$type<
|
||||||
|
| "monthly"
|
||||||
|
| "yearly"
|
||||||
|
| "lifetime"
|
||||||
|
| "daily"
|
||||||
|
| "hourly"
|
||||||
|
| "weekly"
|
||||||
|
>()
|
||||||
|
.notNull()
|
||||||
|
.default("monthly"),
|
||||||
|
enforcement: text("enforcement")
|
||||||
|
.$type<"hard" | "soft">()
|
||||||
|
.notNull()
|
||||||
|
.default("hard"),
|
||||||
|
enabled: integer("enabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(true),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
updatedAt: integer("updatedAt").notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
unique("ai_budget_provider_uniq").on(t.providerId, t.unit, t.period),
|
||||||
|
unique("ai_budget_model_uniq").on(t.modelId, t.unit, t.period),
|
||||||
|
unique("ai_budget_resource_uniq").on(t.resourceId, t.unit, t.period),
|
||||||
|
unique("ai_budget_site_resource_uniq").on(
|
||||||
|
t.siteResourceId,
|
||||||
|
t.unit,
|
||||||
|
t.period
|
||||||
|
),
|
||||||
|
unique("ai_budget_role_uniq").on(t.roleId, t.unit, t.period),
|
||||||
|
unique("ai_budget_virtual_api_key_uniq").on(
|
||||||
|
t.virtualApiKeyId,
|
||||||
|
t.unit,
|
||||||
|
t.period
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const aiUsageRecords = sqliteTable(
|
||||||
|
"aiUsageRecords",
|
||||||
|
{
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
providerId: integer("providerId").references(
|
||||||
|
() => aiProviders.providerId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
resourceId: integer("resourceId").references(
|
||||||
|
() => resources.resourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
virtualApiKeyId: text("virtualApiKeyId").references(
|
||||||
|
() => virtualApiKeys.virtualApiKeyId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
// Links this usage record back to the aiSessionLog row for the same
|
||||||
|
// request (aiSessionLog.sessionId), so token/cost usage can be shown
|
||||||
|
// alongside the session transcript. Not a DB-level FK - aiSessionLog
|
||||||
|
// lives in the separate logs database. Nullable because the session
|
||||||
|
// log may be disabled (retention set to 0) while usage tracking
|
||||||
|
// stays on.
|
||||||
|
sessionId: text("sessionId"),
|
||||||
|
requestedModel: text("requestedModel").notNull(),
|
||||||
|
promptTokens: integer("promptTokens").notNull().default(0),
|
||||||
|
cacheReadTokens: integer("cacheReadTokens").notNull().default(0),
|
||||||
|
cacheWriteTokens: integer("cacheWriteTokens").notNull().default(0),
|
||||||
|
completionTokens: integer("completionTokens").notNull().default(0),
|
||||||
|
reasoningTokens: integer("reasoningTokens").notNull().default(0),
|
||||||
|
totalTokens: integer("totalTokens").notNull().default(0),
|
||||||
|
costUsd: real("costUsd"),
|
||||||
|
estimated: integer("estimated", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
createdAt: integer("createdAt").notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_ai_usage_records_org_provider_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.providerId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_org_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.resourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_org_site_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.siteResourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_org_user_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.userId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_org_virtual_api_key_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.virtualApiKeyId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_usage_records_session").on(t.sessionId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const aiBudgetBreachEvents = sqliteTable(
|
||||||
|
"aiBudgetBreachEvents",
|
||||||
|
{
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
budgetId: integer("budgetId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => aiBudgets.budgetId, { onDelete: "cascade" }),
|
||||||
|
enforcement: text("enforcement").$type<"hard" | "soft">().notNull(),
|
||||||
|
unit: text("unit").$type<"usd" | "tokens">().notNull(),
|
||||||
|
period: text("period")
|
||||||
|
.$type<
|
||||||
|
| "monthly"
|
||||||
|
| "yearly"
|
||||||
|
| "lifetime"
|
||||||
|
| "daily"
|
||||||
|
| "hourly"
|
||||||
|
| "weekly"
|
||||||
|
>()
|
||||||
|
.notNull(),
|
||||||
|
amount: real("amount").notNull(),
|
||||||
|
usageAmount: real("usageAmount").notNull(),
|
||||||
|
blocked: integer("blocked", { mode: "boolean" }).notNull(),
|
||||||
|
requestUserId: text("requestUserId").references(() => users.userId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
createdAt: integer("createdAt").notNull()
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_ai_budget_breach_events_budget_created").on(
|
||||||
|
t.budgetId,
|
||||||
|
t.createdAt
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Logs the aggregated prompt + response for a single AI gateway request, for
|
||||||
|
// session replay. One row per request (not per streaming chunk). `sessionId`
|
||||||
|
// is a fresh random id per row for now - no cross-request correlation yet,
|
||||||
|
// but the column exists so a future pass can link multiple rows into a real
|
||||||
|
// multi-turn session.
|
||||||
|
export const aiSessionLog = sqliteTable(
|
||||||
|
"aiSessionLog",
|
||||||
|
{
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
sessionId: text("sessionId").notNull(),
|
||||||
|
orgId: text("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
providerId: integer("providerId").references(
|
||||||
|
() => aiProviders.providerId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
capability: text("capability").notNull(),
|
||||||
|
resourceId: integer("resourceId").references(
|
||||||
|
() => resources.resourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
virtualApiKeyId: text("virtualApiKeyId").references(
|
||||||
|
() => virtualApiKeys.virtualApiKeyId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
requestedModel: text("requestedModel"),
|
||||||
|
isStream: integer("isStream", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
requestBody: text("requestBody"),
|
||||||
|
responseBody: text("responseBody"),
|
||||||
|
// Capability-agnostic message transcript (JSON-encoded
|
||||||
|
// NormalizedAiMessage[] from server/lib/aiMessageNormalization.ts),
|
||||||
|
// computed at write time so search/display never need per-capability
|
||||||
|
// parsing logic. Null when normalization couldn't recognize the
|
||||||
|
// shape - callers fall back to requestBody/responseBody.
|
||||||
|
normalizedRequest: text("normalizedRequest"),
|
||||||
|
normalizedResponse: text("normalizedResponse"),
|
||||||
|
// True if any of the request/response (raw or normalized) fields
|
||||||
|
// were cut short at AI_SESSION_LOG_MAX_BODY_CHARS before storage.
|
||||||
|
truncated: integer("truncated", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
statusCode: integer("statusCode"),
|
||||||
|
createdAt: integer("createdAt").notNull() // epoch ms
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt),
|
||||||
|
index("idx_ai_session_log_org_provider_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.providerId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.resourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_site_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.siteResourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_user_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.userId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_virtual_api_key_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.virtualApiKeyId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_session").on(t.sessionId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const certificates = sqliteTable("certificates", {
|
||||||
|
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||||
|
domain: text("domain").notNull().unique(),
|
||||||
|
domainId: text("domainId").references(() => domains.domainId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
wildcard: integer("wildcard", { mode: "boolean" }).default(false),
|
||||||
|
status: text("status").notNull().default("pending"), // pending, requested, valid, expired, failed
|
||||||
|
expiresAt: integer("expiresAt"),
|
||||||
|
lastRenewalAttempt: integer("lastRenewalAttempt"),
|
||||||
|
createdAt: integer("createdAt").notNull(),
|
||||||
|
updatedAt: integer("updatedAt").notNull(),
|
||||||
|
orderId: text("orderId"),
|
||||||
|
errorMessage: text("errorMessage"),
|
||||||
|
renewalCount: integer("renewalCount").default(0),
|
||||||
|
certFile: text("certFile"),
|
||||||
|
keyFile: text("keyFile")
|
||||||
|
});
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -1577,6 +2083,10 @@ export type Idp = InferSelectModel<typeof idp>;
|
|||||||
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||||
|
export type VirtualApiKey = InferSelectModel<typeof virtualApiKeys>;
|
||||||
|
export type VirtualApiKeyResource = InferSelectModel<
|
||||||
|
typeof virtualApiKeyResources
|
||||||
|
>;
|
||||||
export type SiteResource = InferSelectModel<typeof siteResources>;
|
export type SiteResource = InferSelectModel<typeof siteResources>;
|
||||||
export type Network = InferSelectModel<typeof networks>;
|
export type Network = InferSelectModel<typeof networks>;
|
||||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||||
@@ -1608,3 +2118,16 @@ export type ResourcePolicyHeaderAuth = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
|
||||||
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
export type UserPolicy = InferSelectModel<typeof userPolicies>;
|
||||||
|
export type AiProvider = InferSelectModel<typeof aiProviders>;
|
||||||
|
export type AiModel = InferSelectModel<typeof aiModels>;
|
||||||
|
export type AiBudget = InferSelectModel<typeof aiBudgets>;
|
||||||
|
export type AiUsageRecord = InferSelectModel<typeof aiUsageRecords>;
|
||||||
|
export type AiBudgetBreachEvent = InferSelectModel<typeof aiBudgetBreachEvents>;
|
||||||
|
export type AiSessionLog = InferSelectModel<typeof aiSessionLog>;
|
||||||
|
export type ResourceAiProvider = InferSelectModel<typeof resourceAiProviders>;
|
||||||
|
export type SiteResourceAiProvider = InferSelectModel<
|
||||||
|
typeof siteResourceAiProviders
|
||||||
|
>;
|
||||||
|
export type ResourceAiModel = InferSelectModel<typeof resourceAiModels>;
|
||||||
|
export type SiteResourceAiModel = InferSelectModel<typeof siteResourceAiModels>;
|
||||||
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||||
|
import { themeColors } from "./lib/theme";
|
||||||
|
import {
|
||||||
|
EmailContainer,
|
||||||
|
EmailFooter,
|
||||||
|
EmailGreeting,
|
||||||
|
EmailHeading,
|
||||||
|
EmailInfoSection,
|
||||||
|
EmailLetterHead,
|
||||||
|
EmailSection,
|
||||||
|
EmailSignature,
|
||||||
|
EmailText
|
||||||
|
} from "./components/Email";
|
||||||
|
|
||||||
|
type IdentityApiKeyGeneratedProps = {
|
||||||
|
orgName: string;
|
||||||
|
accountLabel?: string | null;
|
||||||
|
credential: string;
|
||||||
|
resourceUrls: string[];
|
||||||
|
hasMoreResources: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IdentityApiKeyGenerated = ({
|
||||||
|
orgName,
|
||||||
|
accountLabel,
|
||||||
|
credential,
|
||||||
|
resourceUrls,
|
||||||
|
hasMoreResources
|
||||||
|
}: IdentityApiKeyGeneratedProps) => {
|
||||||
|
const previewText = `Your personal identity key for ${orgName}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind config={themeColors}>
|
||||||
|
<Body className="font-sans bg-gray-50">
|
||||||
|
<EmailContainer>
|
||||||
|
<EmailLetterHead />
|
||||||
|
|
||||||
|
<EmailGreeting>Hi there,</EmailGreeting>
|
||||||
|
|
||||||
|
<EmailText>
|
||||||
|
This is your personal identity key for{" "}
|
||||||
|
<strong>{orgName}</strong>. It belongs to your
|
||||||
|
account and identifies you when you use public AI
|
||||||
|
gateways.
|
||||||
|
</EmailText>
|
||||||
|
|
||||||
|
<EmailText>
|
||||||
|
Use it with resources your administrator has granted
|
||||||
|
you, or that your role has access to. Treat this key
|
||||||
|
like a password and do not share it.
|
||||||
|
</EmailText>
|
||||||
|
|
||||||
|
<EmailSection>
|
||||||
|
<EmailText>Your identity key:</EmailText>
|
||||||
|
<div className="inline-block max-w-full">
|
||||||
|
<div className="bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 mx-auto text-left">
|
||||||
|
<span className="text-sm font-mono text-gray-900 break-all">
|
||||||
|
{credential}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</EmailSection>
|
||||||
|
|
||||||
|
<EmailFooter>
|
||||||
|
<EmailSignature />
|
||||||
|
</EmailFooter>
|
||||||
|
</EmailContainer>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IdentityApiKeyGenerated;
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
|
||||||
|
import { themeColors } from "./lib/theme";
|
||||||
|
import {
|
||||||
|
EmailContainer,
|
||||||
|
EmailFooter,
|
||||||
|
EmailGreeting,
|
||||||
|
EmailInfoSection,
|
||||||
|
EmailLetterHead,
|
||||||
|
EmailSection,
|
||||||
|
EmailSignature,
|
||||||
|
EmailText
|
||||||
|
} from "./components/Email";
|
||||||
|
|
||||||
|
type VirtualApiKeyGeneratedProps = {
|
||||||
|
orgName: string;
|
||||||
|
keyName: string | null;
|
||||||
|
credential: string;
|
||||||
|
resourceUrls: string[];
|
||||||
|
hasMoreResources: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VirtualApiKeyGenerated = ({
|
||||||
|
orgName,
|
||||||
|
keyName,
|
||||||
|
credential,
|
||||||
|
resourceUrls,
|
||||||
|
hasMoreResources
|
||||||
|
}: VirtualApiKeyGeneratedProps) => {
|
||||||
|
const previewText = `A virtual API key for ${orgName} has been shared with you`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind config={themeColors}>
|
||||||
|
<Body className="font-sans bg-gray-50">
|
||||||
|
<EmailContainer>
|
||||||
|
<EmailLetterHead />
|
||||||
|
|
||||||
|
<EmailGreeting>Hi there,</EmailGreeting>
|
||||||
|
|
||||||
|
<EmailText>
|
||||||
|
A virtual API key for <strong>{orgName}</strong> has
|
||||||
|
been shared with you. This key grants access to the
|
||||||
|
public AI gateways it was created for. Treat this
|
||||||
|
key like a password and do not share it.
|
||||||
|
</EmailText>
|
||||||
|
|
||||||
|
<EmailSection>
|
||||||
|
<EmailText>Your virtual API key:</EmailText>
|
||||||
|
<div className="inline-block max-w-full">
|
||||||
|
<div className="bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 mx-auto text-left">
|
||||||
|
<span className="text-sm font-mono text-gray-900 break-all">
|
||||||
|
{credential}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</EmailSection>
|
||||||
|
|
||||||
|
<EmailInfoSection
|
||||||
|
title="Key details"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: "Organization",
|
||||||
|
value: orgName
|
||||||
|
},
|
||||||
|
...(keyName
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: "Name",
|
||||||
|
value: keyName
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{resourceUrls.length > 0 && (
|
||||||
|
<>
|
||||||
|
<EmailText>
|
||||||
|
This key can be used to authenticate to the
|
||||||
|
following AI gateway resources:
|
||||||
|
</EmailText>
|
||||||
|
<div className="px-6 pb-2">
|
||||||
|
{resourceUrls.map((url) => (
|
||||||
|
<p
|
||||||
|
key={url}
|
||||||
|
className="text-base text-gray-700 leading-relaxed"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
className="text-primary font-medium break-all"
|
||||||
|
>
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{hasMoreResources && (
|
||||||
|
<EmailText>
|
||||||
|
Contact your administrator to get the
|
||||||
|
full list.
|
||||||
|
</EmailText>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<EmailFooter>
|
||||||
|
<EmailSignature />
|
||||||
|
</EmailFooter>
|
||||||
|
</EmailContainer>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VirtualApiKeyGenerated;
|
||||||
@@ -18,7 +18,7 @@ export function EmailLetterHead() {
|
|||||||
<Img
|
<Img
|
||||||
src="https://fossorial-public-assets.s3.us-east-1.amazonaws.com/word_mark_black.png"
|
src="https://fossorial-public-assets.s3.us-east-1.amazonaws.com/word_mark_black.png"
|
||||||
alt="Pangolin Logo"
|
alt="Pangolin Logo"
|
||||||
width="180"
|
width="135"
|
||||||
height="auto"
|
height="auto"
|
||||||
className="mx-auto"
|
className="mx-auto"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+16
-9
@@ -5,26 +5,29 @@ import { runSetupFunctions } from "./setup";
|
|||||||
import { createApiServer } from "./apiServer";
|
import { createApiServer } from "./apiServer";
|
||||||
import { createNextServer } from "./nextServer";
|
import { createNextServer } from "./nextServer";
|
||||||
import { createInternalServer } from "./internalServer";
|
import { createInternalServer } from "./internalServer";
|
||||||
|
import { createAiGatewayServer } from "./aiGatewayServer";
|
||||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||||
import {
|
import {
|
||||||
ApiKey,
|
ApiKey,
|
||||||
ApiKeyOrg,
|
ApiKeyOrg,
|
||||||
|
AiBudget,
|
||||||
|
AiModel,
|
||||||
|
AiProvider,
|
||||||
RemoteExitNode,
|
RemoteExitNode,
|
||||||
Session,
|
Session,
|
||||||
SiteResource,
|
SiteResource,
|
||||||
User,
|
User,
|
||||||
UserOrg
|
UserOrg,
|
||||||
|
VirtualApiKey
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { setHostMeta } from "@server/lib/hostMeta";
|
import { setHostMeta } from "@server/lib/hostMeta";
|
||||||
import { initTelemetryClient } from "@server/lib/telemetry";
|
|
||||||
import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
||||||
import { initCleanup } from "#dynamic/cleanup";
|
import { initCleanup } from "#dynamic/cleanup";
|
||||||
|
import { startSchedulers } from "#dynamic/startSchedulers";
|
||||||
import license from "#dynamic/license/license";
|
import license from "#dynamic/license/license";
|
||||||
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
|
||||||
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
|
|
||||||
import { fetchServerIp } from "@server/lib/serverIpService";
|
import { fetchServerIp } from "@server/lib/serverIpService";
|
||||||
import { startRebuildQueueProcessor } from "@server/lib/rebuildClientAssociations";
|
import { initAiModelCatalog } from "@server/lib/aiModelCatalog";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
@@ -38,15 +41,14 @@ async function startServers() {
|
|||||||
|
|
||||||
await fetchServerIp();
|
await fetchServerIp();
|
||||||
|
|
||||||
initTelemetryClient();
|
await initAiModelCatalog();
|
||||||
|
|
||||||
initLogCleanupInterval();
|
startSchedulers();
|
||||||
initAcmeCertSync();
|
|
||||||
startRebuildQueueProcessor();
|
|
||||||
|
|
||||||
// Start all servers
|
// Start all servers
|
||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
const internalServer = createInternalServer();
|
const internalServer = createInternalServer();
|
||||||
|
const aiGatewayServer = createAiGatewayServer();
|
||||||
|
|
||||||
const nextServer = await createNextServer();
|
const nextServer = await createNextServer();
|
||||||
if (config.getRawConfig().traefik.file_mode) {
|
if (config.getRawConfig().traefik.file_mode) {
|
||||||
@@ -65,6 +67,7 @@ async function startServers() {
|
|||||||
apiServer,
|
apiServer,
|
||||||
nextServer,
|
nextServer,
|
||||||
internalServer,
|
internalServer,
|
||||||
|
aiGatewayServer,
|
||||||
integrationServer
|
integrationServer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -83,6 +86,10 @@ declare global {
|
|||||||
userOrgIds?: string[];
|
userOrgIds?: string[];
|
||||||
remoteExitNode?: RemoteExitNode;
|
remoteExitNode?: RemoteExitNode;
|
||||||
siteResource?: SiteResource;
|
siteResource?: SiteResource;
|
||||||
|
aiProvider?: AiProvider;
|
||||||
|
aiModel?: AiModel;
|
||||||
|
aiBudget?: AiBudget;
|
||||||
|
virtualApiKey?: VirtualApiKey;
|
||||||
orgPolicyAllowed?: boolean;
|
orgPolicyAllowed?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+865
-2
@@ -1,3 +1,866 @@
|
|||||||
export function initAcmeCertSync(): void {
|
import fs from "fs";
|
||||||
// stub
|
import path from "path";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import {
|
||||||
|
certificates,
|
||||||
|
clients,
|
||||||
|
clientSiteResourcesAssociationsCache,
|
||||||
|
db,
|
||||||
|
domains,
|
||||||
|
newts,
|
||||||
|
siteNetworks,
|
||||||
|
SiteResource,
|
||||||
|
siteResources
|
||||||
|
} from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import {
|
||||||
|
generateSubnetProxyTargetV2,
|
||||||
|
SubnetProxyTargetV2
|
||||||
|
} from "@server/lib/ip";
|
||||||
|
import { updateTargets } from "@server/routers/client/targets";
|
||||||
|
import cache from "#dynamic/lib/cache";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
|
interface AcmeCert {
|
||||||
|
domain: { main: string; sans?: string[] };
|
||||||
|
certificate: string;
|
||||||
|
key: string;
|
||||||
|
Store: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AcmeJson {
|
||||||
|
[resolver: string]: {
|
||||||
|
Certificates: AcmeCert[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pushCertUpdateToAffectedNewts(
|
||||||
|
domain: string,
|
||||||
|
domainId: string | null,
|
||||||
|
oldCertPem: string | null,
|
||||||
|
oldKeyPem: string | null
|
||||||
|
): Promise<void> {
|
||||||
|
// Find all SSL-enabled HTTP site resources that use this cert's domain
|
||||||
|
let affectedResources: SiteResource[] = [];
|
||||||
|
|
||||||
|
if (domainId) {
|
||||||
|
affectedResources = await db
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.domainId, domainId),
|
||||||
|
eq(siteResources.ssl, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Fallback: match by exact fullDomain when no domainId is available
|
||||||
|
affectedResources = await db
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.fullDomain, domain),
|
||||||
|
eq(siteResources.ssl, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (affectedResources.length === 0) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no affected site resources for cert domain "${domain}"`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: pushing cert update to ${affectedResources.length} affected site resource(s) for domain "${domain}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const resource of affectedResources) {
|
||||||
|
try {
|
||||||
|
// Get all sites for this resource via siteNetworks
|
||||||
|
const resourceSiteRows = resource.networkId
|
||||||
|
? await db
|
||||||
|
.select({ siteId: siteNetworks.siteId })
|
||||||
|
.from(siteNetworks)
|
||||||
|
.where(eq(siteNetworks.networkId, resource.networkId))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (resourceSiteRows.length === 0) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no sites for resource ${resource.siteResourceId}, skipping`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all clients with access to this resource
|
||||||
|
const resourceClients = await db
|
||||||
|
.select({
|
||||||
|
clientId: clients.clientId,
|
||||||
|
pubKey: clients.pubKey,
|
||||||
|
subnet: clients.subnet
|
||||||
|
})
|
||||||
|
.from(clients)
|
||||||
|
.innerJoin(
|
||||||
|
clientSiteResourcesAssociationsCache,
|
||||||
|
eq(
|
||||||
|
clients.clientId,
|
||||||
|
clientSiteResourcesAssociationsCache.clientId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||||
|
resource.siteResourceId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resourceClients.length === 0) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no clients for resource ${resource.siteResourceId}, skipping`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate the cert cache so generateSubnetProxyTargetV2 fetches fresh data
|
||||||
|
if (resource.fullDomain) {
|
||||||
|
await cache.del(`cert:${resource.fullDomain}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate target once - same cert applies to all sites for this resource
|
||||||
|
const newTargets = await generateSubnetProxyTargetV2(
|
||||||
|
resource,
|
||||||
|
resourceClients
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!newTargets) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not generate target for resource ${resource.siteResourceId}, skipping`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the old targets - same routing shape but with the previous cert/key.
|
||||||
|
// The newt only uses destPrefix/sourcePrefixes for removal, but we keep the
|
||||||
|
// semantics correct so the update message accurately reflects what changed.
|
||||||
|
const oldTargets: SubnetProxyTargetV2[] = newTargets.map((t) => ({
|
||||||
|
...t,
|
||||||
|
tlsCert: oldCertPem ?? undefined,
|
||||||
|
tlsKey: oldKeyPem ?? undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Push update to each site's newt
|
||||||
|
for (const { siteId } of resourceSiteRows) {
|
||||||
|
const [newt] = await db
|
||||||
|
.select()
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!newt) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no newt found for site ${siteId}, skipping resource ${resource.siteResourceId}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateTargets(
|
||||||
|
newt.newtId,
|
||||||
|
{ oldTargets: oldTargets, newTargets: newTargets },
|
||||||
|
newt.version
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: pushed cert update to newt for site ${siteId}, resource ${resource.siteResourceId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`acmeCertSync: error pushing cert update for resource ${resource?.siteResourceId}: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findDomainId(certDomain: string): Promise<string | null> {
|
||||||
|
// Strip wildcard prefix before lookup (*.example.com -> example.com)
|
||||||
|
const lookupDomain = certDomain.startsWith("*.")
|
||||||
|
? certDomain.slice(2)
|
||||||
|
: certDomain;
|
||||||
|
|
||||||
|
// 1. Exact baseDomain match (any domain type)
|
||||||
|
const exactMatch = await db
|
||||||
|
.select({ domainId: domains.domainId })
|
||||||
|
.from(domains)
|
||||||
|
.where(eq(domains.baseDomain, lookupDomain))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (exactMatch.length > 0) {
|
||||||
|
return exactMatch[0].domainId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Walk up the domain hierarchy looking for a wildcard-type domain whose
|
||||||
|
// baseDomain is a suffix of the cert domain. e.g. cert "sub.example.com"
|
||||||
|
// matches a wildcard domain with baseDomain "example.com".
|
||||||
|
const parts = lookupDomain.split(".");
|
||||||
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
const candidate = parts.slice(i).join(".");
|
||||||
|
if (!candidate) continue;
|
||||||
|
|
||||||
|
const wildcardMatch = await db
|
||||||
|
.select({ domainId: domains.domainId })
|
||||||
|
.from(domains)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(domains.baseDomain, candidate),
|
||||||
|
eq(domains.type, "wildcard")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (wildcardMatch.length > 0) {
|
||||||
|
return wildcardMatch[0].domainId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFirstCert(pemBundle: string): string | null {
|
||||||
|
const match = pemBundle.match(
|
||||||
|
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/
|
||||||
|
);
|
||||||
|
return match ? match[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether an ACME cert entry represents a wildcard cert by checking
|
||||||
|
* both the primary domain (`main`) and the SANs. Some ACME clients (notably
|
||||||
|
* Traefik) store the bare apex in `main` and only put the wildcard form in
|
||||||
|
* `sans` (e.g. main="access.example.com", sans=["*.access.example.com"]).
|
||||||
|
*/
|
||||||
|
function detectWildcard(
|
||||||
|
main: string,
|
||||||
|
sans: string[] | undefined
|
||||||
|
): { wildcard: boolean; wildcardSan: string | null } {
|
||||||
|
if (main.startsWith("*.")) {
|
||||||
|
return { wildcard: true, wildcardSan: null };
|
||||||
|
}
|
||||||
|
if (Array.isArray(sans)) {
|
||||||
|
for (const san of sans) {
|
||||||
|
if (typeof san !== "string") continue;
|
||||||
|
if (san === `*.${main}` || san.startsWith("*.")) {
|
||||||
|
return { wildcard: true, wildcardSan: san };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { wildcard: false, wildcardSan: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HttpCert {
|
||||||
|
wildcard: boolean;
|
||||||
|
altName: string;
|
||||||
|
certName: string;
|
||||||
|
commonName: string;
|
||||||
|
certFile: string;
|
||||||
|
keyFile: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncAcmeCertsFromHttp(endpoint: string): Promise<void> {
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(endpoint);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not reach HTTP endpoint ${endpoint}: ${err}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: HTTP endpoint returned status ${response.status}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let httpCerts: HttpCert[];
|
||||||
|
try {
|
||||||
|
httpCerts = await response.json();
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not parse JSON from HTTP endpoint: ${err}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(httpCerts) || httpCerts.length === 0) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no certificates returned from HTTP endpoint`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cert of httpCerts) {
|
||||||
|
const domain = cert?.certName;
|
||||||
|
|
||||||
|
if (!domain || typeof domain !== "string") {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping HTTP cert with missing certName`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const certPem = cert.certFile;
|
||||||
|
const keyPem = cert.keyFile;
|
||||||
|
|
||||||
|
if (!certPem?.trim() || !keyPem?.trim()) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping HTTP cert for ${domain} - empty certFile or keyFile`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstCertPemForValidation = extractFirstCert(certPem);
|
||||||
|
if (!firstCertPemForValidation) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping HTTP cert for ${domain} - no PEM certificate block found`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let validatedX509: crypto.X509Certificate;
|
||||||
|
try {
|
||||||
|
validatedX509 = new crypto.X509Certificate(
|
||||||
|
firstCertPemForValidation
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping HTTP cert for ${domain} - invalid X.509 certificate: ${err}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
crypto.createPrivateKey(keyPem);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping HTTP cert for ${domain} - invalid private key: ${err}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wildcard = cert.wildcard ?? false;
|
||||||
|
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(certificates)
|
||||||
|
.where(eq(certificates.domain, domain))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let oldCertPem: string | null = null;
|
||||||
|
let oldKeyPem: string | null = null;
|
||||||
|
|
||||||
|
if (existing.length > 0 && existing[0].certFile) {
|
||||||
|
try {
|
||||||
|
const storedCertPem = decrypt(
|
||||||
|
existing[0].certFile,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
const wildcardUnchanged = existing[0].wildcard === wildcard;
|
||||||
|
if (storedCertPem === certPem && wildcardUnchanged) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
oldCertPem = storedCertPem;
|
||||||
|
if (existing[0].keyFile) {
|
||||||
|
try {
|
||||||
|
oldKeyPem = decrypt(
|
||||||
|
existing[0].keyFile,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
} catch (keyErr) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expiresAt: number | null = null;
|
||||||
|
try {
|
||||||
|
expiresAt = Math.floor(
|
||||||
|
new Date(validatedX509.validTo).getTime() / 1000
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedCert = encrypt(
|
||||||
|
certPem,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
const encryptedKey = encrypt(
|
||||||
|
keyPem,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const domainId = await findDomainId(domain);
|
||||||
|
if (domainId) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: resolved domainId "${domainId}" for HTTP cert domain "${domain}"`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no matching domain record found for HTTP cert domain "${domain}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: updating existing certificate (HTTP) for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.update(certificates)
|
||||||
|
.set({
|
||||||
|
certFile: encryptedCert,
|
||||||
|
keyFile: encryptedKey,
|
||||||
|
status: "valid",
|
||||||
|
expiresAt,
|
||||||
|
updatedAt: now,
|
||||||
|
wildcard,
|
||||||
|
...(domainId !== null && { domainId })
|
||||||
|
})
|
||||||
|
.where(eq(certificates.domain, domain));
|
||||||
|
|
||||||
|
await pushCertUpdateToAffectedNewts(
|
||||||
|
domain,
|
||||||
|
domainId,
|
||||||
|
oldCertPem,
|
||||||
|
oldKeyPem
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: inserting new certificate (HTTP) for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
await db.insert(certificates).values({
|
||||||
|
domain,
|
||||||
|
domainId,
|
||||||
|
certFile: encryptedCert,
|
||||||
|
keyFile: encryptedKey,
|
||||||
|
status: "valid",
|
||||||
|
expiresAt,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
wildcard
|
||||||
|
});
|
||||||
|
|
||||||
|
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storeCertForDomain(
|
||||||
|
domain: string,
|
||||||
|
certPem: string,
|
||||||
|
keyPem: string,
|
||||||
|
validatedX509: crypto.X509Certificate
|
||||||
|
): Promise<void> {
|
||||||
|
const wildcard = domain.startsWith("*.");
|
||||||
|
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(certificates)
|
||||||
|
.where(eq(certificates.domain, domain))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let oldCertPem: string | null = null;
|
||||||
|
let oldKeyPem: string | null = null;
|
||||||
|
|
||||||
|
if (existing.length > 0 && existing[0].certFile) {
|
||||||
|
try {
|
||||||
|
const storedCertPem = decrypt(
|
||||||
|
existing[0].certFile,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
const wildcardUnchanged = existing[0].wildcard === wildcard;
|
||||||
|
if (storedCertPem === certPem && wildcardUnchanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
oldCertPem = storedCertPem;
|
||||||
|
if (existing[0].keyFile) {
|
||||||
|
try {
|
||||||
|
oldKeyPem = decrypt(
|
||||||
|
existing[0].keyFile,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
} catch (keyErr) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expiresAt: number | null = null;
|
||||||
|
try {
|
||||||
|
expiresAt = Math.floor(
|
||||||
|
new Date(validatedX509.validTo).getTime() / 1000
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedCert = encrypt(
|
||||||
|
certPem,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
const encryptedKey = encrypt(keyPem, config.getRawConfig().server.secret!);
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const domainId = await findDomainId(domain);
|
||||||
|
if (domainId) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: resolved domainId "${domainId}" for cert domain "${domain}"`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no matching domain record found for cert domain "${domain}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: updating existing certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.update(certificates)
|
||||||
|
.set({
|
||||||
|
certFile: encryptedCert,
|
||||||
|
keyFile: encryptedKey,
|
||||||
|
status: "valid",
|
||||||
|
expiresAt,
|
||||||
|
updatedAt: now,
|
||||||
|
wildcard,
|
||||||
|
...(domainId !== null && { domainId })
|
||||||
|
})
|
||||||
|
.where(eq(certificates.domain, domain));
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: updated certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
|
||||||
|
await pushCertUpdateToAffectedNewts(
|
||||||
|
domain,
|
||||||
|
domainId,
|
||||||
|
oldCertPem,
|
||||||
|
oldKeyPem
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: inserting new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
await db.insert(certificates).values({
|
||||||
|
domain,
|
||||||
|
domainId,
|
||||||
|
certFile: encryptedCert,
|
||||||
|
keyFile: encryptedKey,
|
||||||
|
status: "valid",
|
||||||
|
expiresAt,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
wildcard
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: inserted new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
|
||||||
|
);
|
||||||
|
|
||||||
|
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAcmeJsonFiles(dirPath: string): string[] {
|
||||||
|
const results: string[] = [];
|
||||||
|
let entries: fs.Dirent[];
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`acmeCertSync: could not read directory "${dirPath}": ${err}`
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dirPath, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
results.push(...findAcmeJsonFiles(fullPath));
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
// check if it is a json file
|
||||||
|
if (entry.name.endsWith(".json")) {
|
||||||
|
let raw: string;
|
||||||
|
try {
|
||||||
|
raw = fs.readFileSync(fullPath, "utf8");
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`acmeCertSync: could not read file "${fullPath}": ${err}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: any;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`acmeCertSync: could not parse "${fullPath}" as JSON: ${err}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
|
||||||
|
let raw: string;
|
||||||
|
try {
|
||||||
|
raw = fs.readFileSync(acmeJsonPath, "utf8");
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`acmeCertSync: could not read "${acmeJsonPath}": ${err}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let acmeJson: AcmeJson;
|
||||||
|
try {
|
||||||
|
acmeJson = JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`acmeCertSync: could not parse "${acmeJsonPath}" as JSON: ${err}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvers = Object.keys(acmeJson || {});
|
||||||
|
if (resolvers.length === 0) {
|
||||||
|
logger.debug(`acmeCertSync: no resolvers found in acme.json`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect certificates from every resolver. If the same domain appears in
|
||||||
|
// multiple resolvers, the last one wins (resolvers iterated in object order).
|
||||||
|
const allCerts: AcmeCert[] = [];
|
||||||
|
for (const resolver of resolvers) {
|
||||||
|
const resolverData = acmeJson[resolver];
|
||||||
|
if (!resolverData || !Array.isArray(resolverData.Certificates)) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no certificates found for resolver "${resolver}"`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// logger.debug(
|
||||||
|
// `acmeCertSync: found ${resolverData.Certificates.length} certificate(s) for resolver "${resolver}"`
|
||||||
|
// );
|
||||||
|
for (const cert of resolverData.Certificates) {
|
||||||
|
allCerts.push(cert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cert of allCerts) {
|
||||||
|
const mainDomain = cert?.domain?.main;
|
||||||
|
|
||||||
|
if (!mainDomain || typeof mainDomain !== "string") {
|
||||||
|
logger.debug(`acmeCertSync: skipping cert with missing domain`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cert.certificate || !cert.key) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping cert for ${mainDomain} - empty certificate or key field`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let certPem: string;
|
||||||
|
let keyPem: string;
|
||||||
|
try {
|
||||||
|
certPem = Buffer.from(cert.certificate, "base64").toString("utf8");
|
||||||
|
keyPem = Buffer.from(cert.key, "base64").toString("utf8");
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping cert for ${mainDomain} - failed to base64-decode cert/key: ${err}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!certPem.trim() || !keyPem.trim()) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping cert for ${mainDomain} - blank PEM after base64 decode`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the decoded data actually parses as a real X.509 cert
|
||||||
|
// before we touch the database. This prevents importing partially-written
|
||||||
|
// or corrupted entries from acme.json.
|
||||||
|
const firstCertPemForValidation = extractFirstCert(certPem);
|
||||||
|
if (!firstCertPemForValidation) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping cert for ${mainDomain} - no PEM certificate block found`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let validatedX509: crypto.X509Certificate;
|
||||||
|
try {
|
||||||
|
validatedX509 = new crypto.X509Certificate(
|
||||||
|
firstCertPemForValidation
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping cert for ${mainDomain} - invalid X.509 certificate: ${err}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity-check the private key parses too
|
||||||
|
try {
|
||||||
|
crypto.createPrivateKey(keyPem);
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: skipping cert for ${mainDomain} - invalid private key: ${err}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all domains covered by this cert: main + every SAN.
|
||||||
|
// Each domain gets its own row in the certificates table so that
|
||||||
|
// lookups by any hostname on the cert succeed independently.
|
||||||
|
const allDomains = new Set<string>([mainDomain]);
|
||||||
|
if (Array.isArray(cert.domain?.sans)) {
|
||||||
|
for (const san of cert.domain.sans) {
|
||||||
|
if (typeof san === "string" && san.trim()) {
|
||||||
|
allDomains.add(san.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logger.debug(
|
||||||
|
// `acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}`
|
||||||
|
// );
|
||||||
|
|
||||||
|
for (const domain of allDomains) {
|
||||||
|
try {
|
||||||
|
await storeCertForDomain(
|
||||||
|
domain,
|
||||||
|
certPem,
|
||||||
|
keyPem,
|
||||||
|
validatedX509
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`acmeCertSync: error storing cert for domain "${domain}": ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initAcmeCertSync(): void {
|
||||||
|
if (build == "saas") {
|
||||||
|
logger.debug(`acmeCertSync: skipping ACME cert sync in SaaS build`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configData = config.getRawConfig();
|
||||||
|
|
||||||
|
if (!configData.flags?.enable_acme_cert_sync) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: ACME cert sync is disabled by config flag, skipping`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const acmeJsonPath =
|
||||||
|
configData.acme?.acme_json_path ?? "config/letsencrypt/acme.json";
|
||||||
|
const intervalMs = configData.acme?.sync_interval_ms ?? 5000;
|
||||||
|
const httpEndpoint = configData.acme?.acme_http_endpoint;
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: starting ACME cert sync from "${acmeJsonPath}" across all resolvers every ${intervalMs}ms`
|
||||||
|
);
|
||||||
|
if (httpEndpoint) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: also syncing from HTTP endpoint "${httpEndpoint}" every ${intervalMs}ms`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const runSync = () => {
|
||||||
|
if (httpEndpoint) {
|
||||||
|
syncAcmeCertsFromHttp(httpEndpoint).catch((err) => {
|
||||||
|
logger.error(`acmeCertSync: error during HTTP sync: ${err}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// only run the file-based sync if the HTTP endpoint is not configured, to avoid doubling up
|
||||||
|
let stat: fs.Stats | null = null;
|
||||||
|
try {
|
||||||
|
stat = fs.statSync(acmeJsonPath);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
`acmeCertSync: cannot stat path "${acmeJsonPath}": ${err}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
const files = findAcmeJsonFiles(acmeJsonPath);
|
||||||
|
if (files.length === 0) {
|
||||||
|
logger.debug(
|
||||||
|
`acmeCertSync: no acme.json files found in directory "${acmeJsonPath}"`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// logger.debug(
|
||||||
|
// `acmeCertSync: found ${files.length} acme.json file(s) in directory "${acmeJsonPath}"`
|
||||||
|
// );
|
||||||
|
for (const file of files) {
|
||||||
|
syncAcmeCerts(file).catch((err) => {
|
||||||
|
logger.error(
|
||||||
|
`acmeCertSync: error during sync of "${file}": ${err}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
syncAcmeCerts(acmeJsonPath).catch((err) => {
|
||||||
|
logger.error(`acmeCertSync: error during sync: ${err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run immediately on init, then on the configured interval
|
||||||
|
runSync();
|
||||||
|
|
||||||
|
setInterval(runSync, intervalMs);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,613 @@
|
|||||||
|
import {
|
||||||
|
and,
|
||||||
|
eq,
|
||||||
|
gte,
|
||||||
|
inArray,
|
||||||
|
isNull,
|
||||||
|
or,
|
||||||
|
sql,
|
||||||
|
SQL,
|
||||||
|
type InferInsertModel
|
||||||
|
} from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
AiBudget,
|
||||||
|
aiBudgetBreachEvents,
|
||||||
|
aiBudgets,
|
||||||
|
aiModels,
|
||||||
|
aiUsageRecords,
|
||||||
|
db,
|
||||||
|
userOrgRoles
|
||||||
|
} from "@server/db";
|
||||||
|
import { modelKeyMatches } from "@server/lib/aiModelKeyMatch";
|
||||||
|
import type { AiUsage } from "@server/lib/aiUsageExtraction";
|
||||||
|
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
type BudgetPeriod = AiBudget["period"];
|
||||||
|
|
||||||
|
const PERIOD_DURATIONS_MS: Record<Exclude<BudgetPeriod, "lifetime">, number> = {
|
||||||
|
hourly: 60 * 60 * 1000,
|
||||||
|
daily: 24 * 60 * 60 * 1000,
|
||||||
|
weekly: 7 * 24 * 60 * 60 * 1000,
|
||||||
|
monthly: 30 * 24 * 60 * 60 * 1000,
|
||||||
|
yearly: 365 * 24 * 60 * 60 * 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Budgets are cheap to be a little stale about (enforcement is already
|
||||||
|
// check-then-act, not transactional). Re-derive each budget's usage sum
|
||||||
|
// from aiUsageRecords at most this often; in between, completed requests
|
||||||
|
// just add their own contribution onto the cached sum instead of
|
||||||
|
// re-querying/re-aggregating from scratch.
|
||||||
|
const BUDGET_CACHE_REFRESH_MS = 8_000;
|
||||||
|
// Redis-level TTL is only a safety net for eviction if a budget stops
|
||||||
|
// seeing traffic - the actual staleness check is the computedAt timestamp
|
||||||
|
// stored in the cached value, compared against BUDGET_CACHE_REFRESH_MS.
|
||||||
|
const BUDGET_CACHE_SAFETY_TTL_SEC = 60;
|
||||||
|
|
||||||
|
function applicableBudgetsCacheKey(ctx: BudgetScopeContext): string {
|
||||||
|
const roleKey = [...ctx.roleIds].sort((a, b) => a - b).join(",");
|
||||||
|
return [
|
||||||
|
"aiBudget:applicable",
|
||||||
|
ctx.orgId,
|
||||||
|
ctx.providerId,
|
||||||
|
ctx.requestedModel,
|
||||||
|
ctx.resourceId ?? "",
|
||||||
|
ctx.siteResourceId ?? "",
|
||||||
|
roleKey,
|
||||||
|
ctx.virtualApiKeyId ?? ""
|
||||||
|
].join(":");
|
||||||
|
}
|
||||||
|
|
||||||
|
function budgetUsageCacheKey(budgetId: number): string {
|
||||||
|
return `aiBudget:usage:${budgetId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CachedBudgetUsage = {
|
||||||
|
sum: number;
|
||||||
|
computedAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Budget periods are trailing windows from "now", not calendar-aligned
|
||||||
|
// (e.g. "daily" = last 24h). "lifetime" has no lower bound.
|
||||||
|
function windowStart(period: BudgetPeriod, now: number): number {
|
||||||
|
if (period === "lifetime") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return now - PERIOD_DURATIONS_MS[period];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BudgetScopeContext = {
|
||||||
|
orgId: string;
|
||||||
|
providerId: number;
|
||||||
|
requestedModel: string;
|
||||||
|
resourceId: number | null;
|
||||||
|
siteResourceId: number | null;
|
||||||
|
roleIds: number[];
|
||||||
|
requestUserId: string | null;
|
||||||
|
virtualApiKeyId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every budget that could apply to this request: the provider itself, any
|
||||||
|
* model on that provider whose (possibly wildcarded) modelKey matches the
|
||||||
|
* requested model, the target resource/site-resource, and any role the
|
||||||
|
* requesting user holds in the org. Cached for BUDGET_CACHE_REFRESH_MS since
|
||||||
|
* budget/model config changes are rare and a request-scoped org/provider/
|
||||||
|
* model/resource/role combination repeats constantly under real traffic.
|
||||||
|
*/
|
||||||
|
export async function resolveApplicableBudgets(
|
||||||
|
ctx: BudgetScopeContext
|
||||||
|
): Promise<AiBudget[]> {
|
||||||
|
const cacheKey = applicableBudgetsCacheKey(ctx);
|
||||||
|
const cached = await cache.get<AiBudget[]>(cacheKey);
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const budgets = await fetchApplicableBudgets(ctx);
|
||||||
|
await cache.set(cacheKey, budgets, BUDGET_CACHE_REFRESH_MS / 1000);
|
||||||
|
return budgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchApplicableBudgets(
|
||||||
|
ctx: BudgetScopeContext
|
||||||
|
): Promise<AiBudget[]> {
|
||||||
|
const providerModels = await db
|
||||||
|
.select({ modelId: aiModels.modelId, modelKey: aiModels.modelKey })
|
||||||
|
.from(aiModels)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiModels.providerId, ctx.providerId),
|
||||||
|
eq(aiModels.enabled, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const matchingModelIds = providerModels
|
||||||
|
.filter((m) => modelKeyMatches(m.modelKey, ctx.requestedModel))
|
||||||
|
.map((m) => m.modelId);
|
||||||
|
|
||||||
|
const scopeConditions: SQL[] = [
|
||||||
|
and(
|
||||||
|
eq(aiBudgets.providerId, ctx.providerId),
|
||||||
|
isNull(aiBudgets.modelId)
|
||||||
|
)!
|
||||||
|
];
|
||||||
|
if (matchingModelIds.length > 0) {
|
||||||
|
scopeConditions.push(inArray(aiBudgets.modelId, matchingModelIds));
|
||||||
|
}
|
||||||
|
if (ctx.resourceId != null) {
|
||||||
|
scopeConditions.push(eq(aiBudgets.resourceId, ctx.resourceId));
|
||||||
|
}
|
||||||
|
if (ctx.siteResourceId != null) {
|
||||||
|
scopeConditions.push(eq(aiBudgets.siteResourceId, ctx.siteResourceId));
|
||||||
|
}
|
||||||
|
if (ctx.roleIds.length > 0) {
|
||||||
|
scopeConditions.push(inArray(aiBudgets.roleId, ctx.roleIds));
|
||||||
|
}
|
||||||
|
if (ctx.virtualApiKeyId != null) {
|
||||||
|
scopeConditions.push(
|
||||||
|
eq(aiBudgets.virtualApiKeyId, ctx.virtualApiKeyId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(aiBudgets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiBudgets.orgId, ctx.orgId),
|
||||||
|
eq(aiBudgets.enabled, true),
|
||||||
|
or(...scopeConditions)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sumUsageAmount(
|
||||||
|
where: SQL,
|
||||||
|
unit: AiBudget["unit"]
|
||||||
|
): Promise<number> {
|
||||||
|
const column =
|
||||||
|
unit === "usd" ? aiUsageRecords.costUsd : aiUsageRecords.totalTokens;
|
||||||
|
const [row] = await db
|
||||||
|
.select({ total: sql<number>`coalesce(sum(${column}), 0)` })
|
||||||
|
.from(aiUsageRecords)
|
||||||
|
.where(where);
|
||||||
|
return Number(row?.total ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sums recorded usage for a single budget's scope + rolling window. Model
|
||||||
|
* budgets can't be pushed down to SQL because the model's key may itself be
|
||||||
|
* a glob, so those rows are fetched for the provider+window and matched in
|
||||||
|
* JS the same way access-control matching does.
|
||||||
|
*/
|
||||||
|
export async function sumUsageForBudget(
|
||||||
|
budget: AiBudget,
|
||||||
|
ctx: BudgetScopeContext,
|
||||||
|
now: number
|
||||||
|
): Promise<number> {
|
||||||
|
const start = windowStart(budget.period, now);
|
||||||
|
|
||||||
|
if (budget.modelId != null) {
|
||||||
|
const [model] = await db
|
||||||
|
.select({
|
||||||
|
providerId: aiModels.providerId,
|
||||||
|
modelKey: aiModels.modelKey
|
||||||
|
})
|
||||||
|
.from(aiModels)
|
||||||
|
.where(eq(aiModels.modelId, budget.modelId))
|
||||||
|
.limit(1);
|
||||||
|
if (!model) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
requestedModel: aiUsageRecords.requestedModel,
|
||||||
|
costUsd: aiUsageRecords.costUsd,
|
||||||
|
totalTokens: aiUsageRecords.totalTokens
|
||||||
|
})
|
||||||
|
.from(aiUsageRecords)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiUsageRecords.orgId, ctx.orgId),
|
||||||
|
eq(aiUsageRecords.providerId, model.providerId),
|
||||||
|
gte(aiUsageRecords.createdAt, start)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return rows
|
||||||
|
.filter((r) => modelKeyMatches(model.modelKey, r.requestedModel))
|
||||||
|
.reduce(
|
||||||
|
(sum, r) =>
|
||||||
|
sum +
|
||||||
|
(budget.unit === "usd" ? (r.costUsd ?? 0) : r.totalTokens),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (budget.providerId != null) {
|
||||||
|
return sumUsageAmount(
|
||||||
|
and(
|
||||||
|
eq(aiUsageRecords.orgId, ctx.orgId),
|
||||||
|
eq(aiUsageRecords.providerId, budget.providerId),
|
||||||
|
gte(aiUsageRecords.createdAt, start)
|
||||||
|
)!,
|
||||||
|
budget.unit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (budget.resourceId != null) {
|
||||||
|
return sumUsageAmount(
|
||||||
|
and(
|
||||||
|
eq(aiUsageRecords.orgId, ctx.orgId),
|
||||||
|
eq(aiUsageRecords.resourceId, budget.resourceId),
|
||||||
|
gte(aiUsageRecords.createdAt, start)
|
||||||
|
)!,
|
||||||
|
budget.unit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (budget.siteResourceId != null) {
|
||||||
|
return sumUsageAmount(
|
||||||
|
and(
|
||||||
|
eq(aiUsageRecords.orgId, ctx.orgId),
|
||||||
|
eq(aiUsageRecords.siteResourceId, budget.siteResourceId),
|
||||||
|
gte(aiUsageRecords.createdAt, start)
|
||||||
|
)!,
|
||||||
|
budget.unit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (budget.roleId != null) {
|
||||||
|
const members = await db
|
||||||
|
.select({ userId: userOrgRoles.userId })
|
||||||
|
.from(userOrgRoles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgRoles.roleId, budget.roleId),
|
||||||
|
eq(userOrgRoles.orgId, ctx.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const userIds = members.map((m) => m.userId);
|
||||||
|
if (userIds.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return sumUsageAmount(
|
||||||
|
and(
|
||||||
|
eq(aiUsageRecords.orgId, ctx.orgId),
|
||||||
|
inArray(aiUsageRecords.userId, userIds),
|
||||||
|
gte(aiUsageRecords.createdAt, start)
|
||||||
|
)!,
|
||||||
|
budget.unit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (budget.virtualApiKeyId != null) {
|
||||||
|
return sumUsageAmount(
|
||||||
|
and(
|
||||||
|
eq(aiUsageRecords.orgId, ctx.orgId),
|
||||||
|
eq(aiUsageRecords.virtualApiKeyId, budget.virtualApiKeyId),
|
||||||
|
gte(aiUsageRecords.createdAt, start)
|
||||||
|
)!,
|
||||||
|
budget.unit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached wrapper around sumUsageForBudget. Reuses a per-budget cached sum
|
||||||
|
* for up to BUDGET_CACHE_REFRESH_MS, and otherwise falls through to the DB
|
||||||
|
* aggregation and reseeds the cache. Completed requests within that window
|
||||||
|
* top the cached sum up via applyUsageToBudgetCache below rather than
|
||||||
|
* forcing a re-aggregation on every request.
|
||||||
|
*/
|
||||||
|
async function getBudgetUsage(
|
||||||
|
budget: AiBudget,
|
||||||
|
ctx: BudgetScopeContext,
|
||||||
|
now: number
|
||||||
|
): Promise<number> {
|
||||||
|
const cacheKey = budgetUsageCacheKey(budget.budgetId);
|
||||||
|
const cached = await cache.get<CachedBudgetUsage>(cacheKey);
|
||||||
|
if (cached && now - cached.computedAt < BUDGET_CACHE_REFRESH_MS) {
|
||||||
|
return cached.sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sum = await sumUsageForBudget(budget, ctx, now);
|
||||||
|
await cache.set(
|
||||||
|
cacheKey,
|
||||||
|
{ sum, computedAt: now } satisfies CachedBudgetUsage,
|
||||||
|
BUDGET_CACHE_SAFETY_TTL_SEC
|
||||||
|
);
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called once a request's actual usage is known, for every budget that was
|
||||||
|
* resolved as applicable to it (i.e. checkBudgets' returned `budgets`).
|
||||||
|
* Adds this request's contribution directly onto each budget's cached sum
|
||||||
|
* so the next request in the same refresh window doesn't need to re-query
|
||||||
|
* or re-aggregate. If there's no warm cache entry, or it's already due for
|
||||||
|
* a refresh, this is a no-op - the next reader re-derives from the DB,
|
||||||
|
* which by then already includes this request's row via recordUsage.
|
||||||
|
*/
|
||||||
|
export async function applyUsageToBudgetCache(
|
||||||
|
budgets: AiBudget[],
|
||||||
|
usage: { usd: number; tokens: number }
|
||||||
|
): Promise<void> {
|
||||||
|
await Promise.all(
|
||||||
|
budgets.map(async (budget) => {
|
||||||
|
const delta = budget.unit === "usd" ? usage.usd : usage.tokens;
|
||||||
|
if (!delta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = budgetUsageCacheKey(budget.budgetId);
|
||||||
|
const cached = await cache.get<CachedBudgetUsage>(cacheKey);
|
||||||
|
if (
|
||||||
|
!cached ||
|
||||||
|
Date.now() - cached.computedAt >= BUDGET_CACHE_REFRESH_MS
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await cache.set(
|
||||||
|
cacheKey,
|
||||||
|
{
|
||||||
|
sum: cached.sum + delta,
|
||||||
|
computedAt: cached.computedAt
|
||||||
|
} satisfies CachedBudgetUsage,
|
||||||
|
BUDGET_CACHE_SAFETY_TTL_SEC
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttled to one durable event per budget per breach window, so a soft
|
||||||
|
// budget being exceeded doesn't write a row on every subsequent request
|
||||||
|
// while it stays over.
|
||||||
|
async function recordBreachEventIfNew(
|
||||||
|
budget: AiBudget,
|
||||||
|
ctx: BudgetScopeContext,
|
||||||
|
usageAmount: number,
|
||||||
|
now: number
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const start = windowStart(budget.period, now);
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ id: aiBudgetBreachEvents.id })
|
||||||
|
.from(aiBudgetBreachEvents)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiBudgetBreachEvents.budgetId, budget.budgetId),
|
||||||
|
gte(aiBudgetBreachEvents.createdAt, start)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
if (existing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(aiBudgetBreachEvents).values({
|
||||||
|
orgId: ctx.orgId,
|
||||||
|
budgetId: budget.budgetId,
|
||||||
|
enforcement: budget.enforcement,
|
||||||
|
unit: budget.unit,
|
||||||
|
period: budget.period,
|
||||||
|
amount: budget.amount,
|
||||||
|
usageAmount,
|
||||||
|
blocked: budget.enforcement === "hard",
|
||||||
|
requestUserId: ctx.requestUserId,
|
||||||
|
createdAt: now
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to record AI budget breach event", {
|
||||||
|
error,
|
||||||
|
budgetId: budget.budgetId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BudgetCheckResult = {
|
||||||
|
blocked: boolean;
|
||||||
|
blockingBudget?: AiBudget;
|
||||||
|
// Every budget resolved as applicable to this request, regardless of
|
||||||
|
// whether it was breached - pass to applyUsageToBudgetCache once this
|
||||||
|
// request's actual usage is known.
|
||||||
|
budgets: AiBudget[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function checkBudgets(
|
||||||
|
ctx: BudgetScopeContext
|
||||||
|
): Promise<BudgetCheckResult> {
|
||||||
|
const budgets = await resolveApplicableBudgets(ctx);
|
||||||
|
if (budgets.length === 0) {
|
||||||
|
return { blocked: false, budgets: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
let blockingBudget: AiBudget | undefined;
|
||||||
|
|
||||||
|
for (const budget of budgets) {
|
||||||
|
const usage = await getBudgetUsage(budget, ctx, now);
|
||||||
|
if (usage < budget.amount) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await recordBreachEventIfNew(budget, ctx, usage, now);
|
||||||
|
|
||||||
|
if (budget.enforcement === "hard" && !blockingBudget) {
|
||||||
|
blockingBudget = budget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockingBudget
|
||||||
|
? { blocked: true, blockingBudget, budgets }
|
||||||
|
: { blocked: false, budgets };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UsageRecordInput = {
|
||||||
|
orgId: string;
|
||||||
|
providerId: number;
|
||||||
|
resourceId: number | null;
|
||||||
|
siteResourceId: number | null;
|
||||||
|
userId: string | null;
|
||||||
|
virtualApiKeyId: string | null;
|
||||||
|
requestedModel: string;
|
||||||
|
usage: AiUsage;
|
||||||
|
costUsd: number | null;
|
||||||
|
createdAt?: number;
|
||||||
|
// Same id as the aiSessionLog row logged for this request, so the two
|
||||||
|
// can be joined to show token/cost usage alongside the session
|
||||||
|
// transcript. Undefined when the session wasn't logged (e.g. session
|
||||||
|
// log retention disabled for the org).
|
||||||
|
sessionId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AiUsageRecordInsert = InferInsertModel<typeof aiUsageRecords>;
|
||||||
|
|
||||||
|
// In-memory buffer for batching AI usage record inserts, mirroring the
|
||||||
|
// approach in server/routers/badger/logRequestAudit.ts. Usage rows are read
|
||||||
|
// back on every budget-cache miss (see getBudgetUsage above), which happens
|
||||||
|
// at least every BUDGET_CACHE_REFRESH_MS, so this buffer is flushed much
|
||||||
|
// more aggressively than the request audit log to keep the table from
|
||||||
|
// lagging behind what budget enforcement needs. Unlike the audit log, there
|
||||||
|
// is no retention/cleanup job for this table - usage history is kept
|
||||||
|
// indefinitely for billing and historical reporting.
|
||||||
|
const usageRecordBuffer: AiUsageRecordInsert[] = [];
|
||||||
|
|
||||||
|
const USAGE_BATCH_SIZE = 20; // Write to DB every 20 records
|
||||||
|
const USAGE_BATCH_INTERVAL_MS = 1000; // Or every 1 second, whichever comes first
|
||||||
|
const USAGE_MAX_BUFFER_SIZE = 5000; // Prevent unbounded memory growth
|
||||||
|
let usageFlushTimer: NodeJS.Timeout | null = null;
|
||||||
|
let isUsageFlushInProgress = false;
|
||||||
|
|
||||||
|
async function flushUsageRecords() {
|
||||||
|
if (usageRecordBuffer.length === 0 || isUsageFlushInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isUsageFlushInProgress = true;
|
||||||
|
|
||||||
|
const recordsToWrite = usageRecordBuffer.splice(
|
||||||
|
0,
|
||||||
|
usageRecordBuffer.length
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use a transaction to ensure all inserts succeed or fail together
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
// Batch insert in groups to avoid overwhelming the database
|
||||||
|
const DB_BATCH_SIZE = 25;
|
||||||
|
for (let i = 0; i < recordsToWrite.length; i += DB_BATCH_SIZE) {
|
||||||
|
const batch = recordsToWrite.slice(i, i + DB_BATCH_SIZE);
|
||||||
|
await tx.insert(aiUsageRecords).values(batch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
logger.debug(
|
||||||
|
`Flushed ${recordsToWrite.length} AI usage records to database`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error flushing AI usage records:", error);
|
||||||
|
// On transaction error, put records back at the front of the buffer
|
||||||
|
// to retry, but only if the buffer isn't too large
|
||||||
|
if (
|
||||||
|
usageRecordBuffer.length <
|
||||||
|
USAGE_MAX_BUFFER_SIZE - recordsToWrite.length
|
||||||
|
) {
|
||||||
|
usageRecordBuffer.unshift(...recordsToWrite);
|
||||||
|
logger.info(
|
||||||
|
`Re-queued ${recordsToWrite.length} AI usage records for retry`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`Buffer full, dropped ${recordsToWrite.length} AI usage records`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isUsageFlushInProgress = false;
|
||||||
|
// If buffer filled up while we were flushing, flush again
|
||||||
|
if (usageRecordBuffer.length >= USAGE_BATCH_SIZE) {
|
||||||
|
flushUsageRecords().catch((err) =>
|
||||||
|
logger.error("Error in follow-up AI usage flush:", err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleUsageFlush() {
|
||||||
|
if (usageFlushTimer === null) {
|
||||||
|
usageFlushTimer = setTimeout(() => {
|
||||||
|
usageFlushTimer = null;
|
||||||
|
flushUsageRecords().catch((err) =>
|
||||||
|
logger.error("Error in scheduled AI usage flush:", err)
|
||||||
|
);
|
||||||
|
}, USAGE_BATCH_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully flush all pending AI usage records (call this on shutdown).
|
||||||
|
*/
|
||||||
|
export async function shutdownUsageRecorder() {
|
||||||
|
if (usageFlushTimer) {
|
||||||
|
clearTimeout(usageFlushTimer);
|
||||||
|
usageFlushTimer = null;
|
||||||
|
}
|
||||||
|
// Force flush even if one is in progress by waiting and retrying
|
||||||
|
while (isUsageFlushInProgress) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
await flushUsageRecords();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function recordUsage(input: UsageRecordInput): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { usage } = input;
|
||||||
|
const totalTokens =
|
||||||
|
usage.promptTokens +
|
||||||
|
usage.cacheReadTokens +
|
||||||
|
usage.cacheWriteTokens +
|
||||||
|
usage.completionTokens +
|
||||||
|
usage.reasoningTokens;
|
||||||
|
|
||||||
|
// Prevent unbounded buffer growth - drop oldest entries if buffer is too large
|
||||||
|
if (usageRecordBuffer.length >= USAGE_MAX_BUFFER_SIZE) {
|
||||||
|
const dropped = usageRecordBuffer.splice(0, USAGE_BATCH_SIZE);
|
||||||
|
logger.warn(
|
||||||
|
`AI usage record buffer exceeded max size (${USAGE_MAX_BUFFER_SIZE}), dropped ${dropped.length} oldest entries`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
usageRecordBuffer.push({
|
||||||
|
orgId: input.orgId,
|
||||||
|
providerId: input.providerId,
|
||||||
|
resourceId: input.resourceId,
|
||||||
|
siteResourceId: input.siteResourceId,
|
||||||
|
userId: input.userId,
|
||||||
|
virtualApiKeyId: input.virtualApiKeyId,
|
||||||
|
sessionId: input.sessionId,
|
||||||
|
requestedModel: input.requestedModel,
|
||||||
|
promptTokens: usage.promptTokens,
|
||||||
|
cacheReadTokens: usage.cacheReadTokens,
|
||||||
|
cacheWriteTokens: usage.cacheWriteTokens,
|
||||||
|
completionTokens: usage.completionTokens,
|
||||||
|
reasoningTokens: usage.reasoningTokens,
|
||||||
|
totalTokens,
|
||||||
|
costUsd: input.costUsd,
|
||||||
|
estimated: usage.estimated,
|
||||||
|
createdAt: input.createdAt ?? Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flush immediately if buffer is full, otherwise schedule a flush
|
||||||
|
if (usageRecordBuffer.length >= USAGE_BATCH_SIZE) {
|
||||||
|
flushUsageRecords().catch((err) =>
|
||||||
|
logger.error("Error flushing AI usage records:", err)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
scheduleUsageFlush();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to record AI usage", { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import type { Request } from "express";
|
||||||
|
import { AI_CAPABILITIES, type AiCapability } from "@app/lib/aiCapabilities";
|
||||||
|
|
||||||
|
export { AI_CAPABILITIES, type AiCapability };
|
||||||
|
|
||||||
|
export type AiCapabilityRoute = {
|
||||||
|
method: "POST";
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AiProtocolFamily = "openai" | "anthropic" | "google" | "bedrock";
|
||||||
|
|
||||||
|
export type AiCapabilityDefinition = {
|
||||||
|
id: AiCapability;
|
||||||
|
protocolFamily: AiProtocolFamily;
|
||||||
|
routes: AiCapabilityRoute[];
|
||||||
|
extractModel: (req: Request) => string | undefined;
|
||||||
|
resolveUpstreamUrl: (
|
||||||
|
baseUrl: string,
|
||||||
|
req: Request,
|
||||||
|
model: string
|
||||||
|
) => string;
|
||||||
|
isStreaming: (req: Request, contentType: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function bodyModel(req: Request): string | undefined {
|
||||||
|
return typeof req.body?.model === "string" ? req.body.model : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function paramModel(req: Request): string | undefined {
|
||||||
|
const model = req.params?.model;
|
||||||
|
return typeof model === "string" && model.length > 0 ? model : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinUpstreamUrl(baseUrl: string, path: string): string {
|
||||||
|
const base = baseUrl.replace(/\/+$/, "");
|
||||||
|
let suffix = path.startsWith("/") ? path : `/${path}`;
|
||||||
|
|
||||||
|
let basePathname = "/";
|
||||||
|
try {
|
||||||
|
basePathname = new URL(base).pathname.replace(/\/+$/, "") || "/";
|
||||||
|
} catch {
|
||||||
|
// Fall through with "/" non-absolute bases are not expected in
|
||||||
|
// production, but keep joining usable for malformed input.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (basePathname !== "/") {
|
||||||
|
const baseSegs = basePathname.split("/").filter(Boolean);
|
||||||
|
const pathSegs = suffix.split("/").filter(Boolean);
|
||||||
|
const max = Math.min(baseSegs.length, pathSegs.length);
|
||||||
|
let overlap = 0;
|
||||||
|
for (let n = max; n >= 1; n--) {
|
||||||
|
const baseSuffix = baseSegs.slice(-n);
|
||||||
|
const pathPrefix = pathSegs.slice(0, n);
|
||||||
|
if (baseSuffix.every((seg, i) => seg === pathPrefix[i])) {
|
||||||
|
overlap = n;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (overlap > 0) {
|
||||||
|
const remaining = pathSegs.slice(overlap);
|
||||||
|
suffix = remaining.length > 0 ? `/${remaining.join("/")}` : "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suffix === "/") {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${base}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathFromRequest(req: Request): string {
|
||||||
|
const raw = req.originalUrl || req.url || req.path;
|
||||||
|
return raw.startsWith("/") ? raw : `/${raw}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bodyRequestsStream(req: Request): boolean {
|
||||||
|
return req.body?.stream === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentTypeIsSse(contentType: string): boolean {
|
||||||
|
return contentType.includes("text/event-stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentTypeIsAmazonEventStream(contentType: string): boolean {
|
||||||
|
return contentType.includes("application/vnd.amazon.eventstream");
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathIncludes(req: Request, fragment: string): boolean {
|
||||||
|
return pathFromRequest(req).includes(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBodyOrSseStreaming(req: Request, contentType: string): boolean {
|
||||||
|
return bodyRequestsStream(req) || contentTypeIsSse(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGeminiStyleStreaming(req: Request, contentType: string): boolean {
|
||||||
|
return (
|
||||||
|
pathIncludes(req, "streamGenerateContent") ||
|
||||||
|
pathIncludes(req, "alt=sse") ||
|
||||||
|
contentTypeIsSse(contentType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AI_CAPABILITY_DEFS: Record<AiCapability, AiCapabilityDefinition> =
|
||||||
|
{
|
||||||
|
openai_chat: {
|
||||||
|
id: "openai_chat",
|
||||||
|
protocolFamily: "openai",
|
||||||
|
routes: [
|
||||||
|
{ method: "POST", path: "/v1/chat/completions" },
|
||||||
|
{ method: "POST", path: "/chat/completions" }
|
||||||
|
],
|
||||||
|
extractModel: bodyModel,
|
||||||
|
resolveUpstreamUrl: (base, req) =>
|
||||||
|
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||||
|
isStreaming: isBodyOrSseStreaming
|
||||||
|
},
|
||||||
|
openai_responses: {
|
||||||
|
id: "openai_responses",
|
||||||
|
protocolFamily: "openai",
|
||||||
|
routes: [{ method: "POST", path: "/v1/responses" }],
|
||||||
|
extractModel: bodyModel,
|
||||||
|
resolveUpstreamUrl: (base, req) =>
|
||||||
|
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||||
|
isStreaming: isBodyOrSseStreaming
|
||||||
|
},
|
||||||
|
anthropic_messages: {
|
||||||
|
id: "anthropic_messages",
|
||||||
|
protocolFamily: "anthropic",
|
||||||
|
routes: [{ method: "POST", path: "/v1/messages" }],
|
||||||
|
extractModel: bodyModel,
|
||||||
|
resolveUpstreamUrl: (base, req) =>
|
||||||
|
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||||
|
isStreaming: isBodyOrSseStreaming
|
||||||
|
},
|
||||||
|
gemini_generate_content: {
|
||||||
|
id: "gemini_generate_content",
|
||||||
|
protocolFamily: "google",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/v1beta/models/:model\\:generateContent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/v1beta/models/:model\\:streamGenerateContent"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
extractModel: paramModel,
|
||||||
|
resolveUpstreamUrl: (base, req) =>
|
||||||
|
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||||
|
isStreaming: isGeminiStyleStreaming
|
||||||
|
},
|
||||||
|
google_generate_content: {
|
||||||
|
id: "google_generate_content",
|
||||||
|
protocolFamily: "google",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
// Vertex publisher model generateContent
|
||||||
|
path: "/v1/projects/:project/locations/:location/publishers/:publisher/models/:model\\:generateContent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/v1/projects/:project/locations/:location/publishers/:publisher/models/:model\\:streamGenerateContent"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
extractModel: paramModel,
|
||||||
|
resolveUpstreamUrl: (base, req) =>
|
||||||
|
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||||
|
isStreaming: isGeminiStyleStreaming
|
||||||
|
},
|
||||||
|
google_raw_predict: {
|
||||||
|
id: "google_raw_predict",
|
||||||
|
protocolFamily: "google",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/v1/projects/:project/locations/:location/publishers/:publisher/models/:model\\:rawPredict"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/v1/projects/:project/locations/:location/publishers/:publisher/models/:model\\:streamRawPredict"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
extractModel: paramModel,
|
||||||
|
resolveUpstreamUrl: (base, req) =>
|
||||||
|
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||||
|
isStreaming: (req, contentType) =>
|
||||||
|
pathIncludes(req, "streamRawPredict") ||
|
||||||
|
pathIncludes(req, "alt=sse") ||
|
||||||
|
contentTypeIsSse(contentType)
|
||||||
|
},
|
||||||
|
bedrock_model_invoke: {
|
||||||
|
id: "bedrock_model_invoke",
|
||||||
|
protocolFamily: "bedrock",
|
||||||
|
routes: [
|
||||||
|
{ method: "POST", path: "/model/:model/invoke" },
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
path: "/model/:model/invoke-with-response-stream"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
extractModel: paramModel,
|
||||||
|
resolveUpstreamUrl: (base, req) =>
|
||||||
|
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||||
|
isStreaming: (req, contentType) =>
|
||||||
|
pathIncludes(req, "invoke-with-response-stream") ||
|
||||||
|
contentTypeIsAmazonEventStream(contentType) ||
|
||||||
|
contentTypeIsSse(contentType)
|
||||||
|
},
|
||||||
|
bedrock_converse: {
|
||||||
|
id: "bedrock_converse",
|
||||||
|
protocolFamily: "bedrock",
|
||||||
|
routes: [
|
||||||
|
{ method: "POST", path: "/model/:model/converse" },
|
||||||
|
{ method: "POST", path: "/model/:model/converse-stream" }
|
||||||
|
],
|
||||||
|
extractModel: paramModel,
|
||||||
|
resolveUpstreamUrl: (base, req) =>
|
||||||
|
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||||
|
isStreaming: (req, contentType) =>
|
||||||
|
pathIncludes(req, "converse-stream") ||
|
||||||
|
contentTypeIsAmazonEventStream(contentType) ||
|
||||||
|
contentTypeIsSse(contentType)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isAiCapability(value: unknown): value is AiCapability {
|
||||||
|
return (
|
||||||
|
typeof value === "string" &&
|
||||||
|
(AI_CAPABILITIES as readonly string[]).includes(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an Express-style route path from AI_CAPABILITY_DEFS into a RegExp.
|
||||||
|
* Handles `:param` segments and escaped literal colons (`\:`).
|
||||||
|
*/
|
||||||
|
export function routePatternToRegExp(routePath: string): RegExp {
|
||||||
|
let pattern = "";
|
||||||
|
for (let i = 0; i < routePath.length; i++) {
|
||||||
|
const ch = routePath[i];
|
||||||
|
if (
|
||||||
|
ch === "\\" &&
|
||||||
|
i + 1 < routePath.length &&
|
||||||
|
routePath[i + 1] === ":"
|
||||||
|
) {
|
||||||
|
pattern += ":";
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === ":") {
|
||||||
|
// Named param: consume until next / or end
|
||||||
|
i++;
|
||||||
|
while (
|
||||||
|
i < routePath.length &&
|
||||||
|
routePath[i] !== "/" &&
|
||||||
|
!(routePath[i] === "\\" && routePath[i + 1] === ":")
|
||||||
|
) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
i--; // loop will ++
|
||||||
|
pattern += "[^/]+";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Escape regex special chars
|
||||||
|
if (/[.*+?^${}()|[\]\\]/.test(ch)) {
|
||||||
|
pattern += "\\" + ch;
|
||||||
|
} else {
|
||||||
|
pattern += ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new RegExp(`^${pattern}$`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAiCapabilityFromPath(path: string): AiCapability | null {
|
||||||
|
const pathname = path.split("?")[0] || "/";
|
||||||
|
const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
||||||
|
|
||||||
|
for (const def of Object.values(AI_CAPABILITY_DEFS)) {
|
||||||
|
for (const route of def.routes) {
|
||||||
|
if (routePatternToRegExp(route.path).test(normalized)) {
|
||||||
|
return def.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCapabilities(raw: unknown): AiCapability[] {
|
||||||
|
if (raw == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: unknown = raw;
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(trimmed);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const out: AiCapability[] = [];
|
||||||
|
const seen = new Set<AiCapability>();
|
||||||
|
for (const item of parsed) {
|
||||||
|
if (isAiCapability(item) && !seen.has(item)) {
|
||||||
|
seen.add(item);
|
||||||
|
out.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeCapabilities(capabilities: AiCapability[]): string {
|
||||||
|
return JSON.stringify(capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function providerHasCapability(
|
||||||
|
capabilities: AiCapability[] | string | null | undefined,
|
||||||
|
capability: AiCapability
|
||||||
|
): boolean {
|
||||||
|
const list =
|
||||||
|
typeof capabilities === "string" || capabilities == null
|
||||||
|
? parseCapabilities(capabilities)
|
||||||
|
: capabilities;
|
||||||
|
return list.includes(capability);
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import {
|
||||||
|
AI_CAPABILITY_DEFS,
|
||||||
|
type AiCapability,
|
||||||
|
type AiProtocolFamily
|
||||||
|
} from "@server/lib/aiCapabilities";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
|
export type ClientErrorResponse = {
|
||||||
|
statusCode?: number;
|
||||||
|
contentType?: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AiCapabilityErrorKind =
|
||||||
|
| "authentication"
|
||||||
|
| "invalid_request"
|
||||||
|
| "not_found"
|
||||||
|
| "permission"
|
||||||
|
| "rate_limit"
|
||||||
|
| "internal";
|
||||||
|
|
||||||
|
const AUTH_MESSAGE = "Invalid API key provided.";
|
||||||
|
|
||||||
|
type KindFields = {
|
||||||
|
openaiType: string;
|
||||||
|
openaiCode: string | null;
|
||||||
|
anthropicType: string;
|
||||||
|
googleStatus: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KIND_FIELDS: Record<AiCapabilityErrorKind, KindFields> = {
|
||||||
|
authentication: {
|
||||||
|
openaiType: "authentication_error",
|
||||||
|
openaiCode: "invalid_api_key",
|
||||||
|
anthropicType: "authentication_error",
|
||||||
|
googleStatus: "UNAUTHENTICATED"
|
||||||
|
},
|
||||||
|
invalid_request: {
|
||||||
|
openaiType: "invalid_request_error",
|
||||||
|
openaiCode: null,
|
||||||
|
anthropicType: "invalid_request_error",
|
||||||
|
googleStatus: "INVALID_ARGUMENT"
|
||||||
|
},
|
||||||
|
not_found: {
|
||||||
|
openaiType: "invalid_request_error",
|
||||||
|
openaiCode: null,
|
||||||
|
anthropicType: "not_found_error",
|
||||||
|
googleStatus: "NOT_FOUND"
|
||||||
|
},
|
||||||
|
permission: {
|
||||||
|
openaiType: "invalid_request_error",
|
||||||
|
openaiCode: null,
|
||||||
|
anthropicType: "permission_error",
|
||||||
|
googleStatus: "PERMISSION_DENIED"
|
||||||
|
},
|
||||||
|
rate_limit: {
|
||||||
|
openaiType: "rate_limit_error",
|
||||||
|
openaiCode: "rate_limit_exceeded",
|
||||||
|
anthropicType: "rate_limit_error",
|
||||||
|
googleStatus: "RESOURCE_EXHAUSTED"
|
||||||
|
},
|
||||||
|
internal: {
|
||||||
|
openaiType: "api_error",
|
||||||
|
openaiCode: null,
|
||||||
|
anthropicType: "api_error",
|
||||||
|
googleStatus: "INTERNAL"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveProtocolFamily(
|
||||||
|
capability: AiCapability | null
|
||||||
|
): AiProtocolFamily {
|
||||||
|
if (capability == null) {
|
||||||
|
return "openai";
|
||||||
|
}
|
||||||
|
return AI_CAPABILITY_DEFS[capability].protocolFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a protocol-native error body for the given capability.
|
||||||
|
* Message stays contextual; only the envelope/machine fields follow the
|
||||||
|
* capability's native API shape.
|
||||||
|
*/
|
||||||
|
export function buildAiCapabilityErrorBody(
|
||||||
|
capability: AiCapability | null,
|
||||||
|
kind: AiCapabilityErrorKind,
|
||||||
|
message: string,
|
||||||
|
httpStatus?: number
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const family = resolveProtocolFamily(capability);
|
||||||
|
const fields = KIND_FIELDS[kind];
|
||||||
|
|
||||||
|
switch (family) {
|
||||||
|
case "openai":
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
message,
|
||||||
|
type: fields.openaiType,
|
||||||
|
param: null,
|
||||||
|
code: fields.openaiCode
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case "anthropic":
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
error: {
|
||||||
|
type: fields.anthropicType,
|
||||||
|
message
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case "google":
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
code: httpStatus ?? HttpCode.BAD_REQUEST,
|
||||||
|
message,
|
||||||
|
status: fields.googleStatus
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case "bedrock":
|
||||||
|
return { message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildInferenceAuthClientError(
|
||||||
|
capability: AiCapability | null
|
||||||
|
): ClientErrorResponse {
|
||||||
|
return {
|
||||||
|
statusCode: HttpCode.UNAUTHORIZED,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(
|
||||||
|
buildAiCapabilityErrorBody(
|
||||||
|
capability,
|
||||||
|
"authentication",
|
||||||
|
AUTH_MESSAGE,
|
||||||
|
HttpCode.UNAUTHORIZED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { createHash } from "crypto";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
|
export const AI_GATEWAY_TRUST_HEADER = "X-Pangolin-Ai-Gateway-Auth";
|
||||||
|
|
||||||
|
// Injected by the same Traefik trust middleware as AI_GATEWAY_TRUST_HEADER,
|
||||||
|
// but its value differs per router (public inference resource vs. private
|
||||||
|
// siteResource) so the gateway can tell which kind of resource a trusted
|
||||||
|
// request arrived on without re-deriving it from resourceId/siteResourceId.
|
||||||
|
export const AI_GATEWAY_RESOURCE_TYPE_HEADER =
|
||||||
|
"X-Pangolin-Ai-Gateway-Resource-Type";
|
||||||
|
|
||||||
|
export type AiGatewayResourceType = "resource" | "site-resource";
|
||||||
|
|
||||||
|
// Opt-in (server.enable_ai_gateway_client_ip_header): carries the client IP
|
||||||
|
// that Badger resolved at the Traefik hop, so it survives an intermediary
|
||||||
|
// proxy between Traefik and the AI gateway that overwrites
|
||||||
|
// X-Forwarded-For/X-Real-Ip instead of appending to them. Set by a
|
||||||
|
// disableForwardAuth Badger middleware instance (see getTraefikConfig.ts)
|
||||||
|
// on the site-resource inference router only, since that's the sole path
|
||||||
|
// that resolves request identity from the client IP.
|
||||||
|
export const AI_GATEWAY_CLIENT_IP_HEADER = "X-Pangolin-Client-Ip";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a Traefik-injected trust token from the server secret.
|
||||||
|
* Traefik overwrites this header on inference routes so the AI gateway can
|
||||||
|
* trust Badger-injected Remote-* identity without re-validating credentials.
|
||||||
|
*/
|
||||||
|
export function deriveAiGatewayTrustToken(secret: string): string {
|
||||||
|
return createHash("sha256")
|
||||||
|
.update(`ai-gateway-trust:${secret}`)
|
||||||
|
.digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAiGatewayTrustToken(): string {
|
||||||
|
const secret = config.getRawConfig().server.secret;
|
||||||
|
if (!secret) {
|
||||||
|
throw new Error("Server secret is required for AI gateway trust token");
|
||||||
|
}
|
||||||
|
return deriveAiGatewayTrustToken(secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAiGatewayTrustHeaderValid(
|
||||||
|
headers: Record<string, string | string[] | undefined> | undefined,
|
||||||
|
expectedToken?: string
|
||||||
|
): boolean {
|
||||||
|
if (!headers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const expected = expectedToken ?? getAiGatewayTrustToken();
|
||||||
|
const raw =
|
||||||
|
headers[AI_GATEWAY_TRUST_HEADER] ??
|
||||||
|
headers[AI_GATEWAY_TRUST_HEADER.toLowerCase()];
|
||||||
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
||||||
|
return typeof value === "string" && value === expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAiGatewayResourceType(
|
||||||
|
headers: Record<string, string | string[] | undefined> | undefined
|
||||||
|
): AiGatewayResourceType | null {
|
||||||
|
if (!headers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const raw =
|
||||||
|
headers[AI_GATEWAY_RESOURCE_TYPE_HEADER] ??
|
||||||
|
headers[AI_GATEWAY_RESOURCE_TYPE_HEADER.toLowerCase()];
|
||||||
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
||||||
|
return value === "resource" || value === "site-resource" ? value : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import http from "node:http";
|
||||||
|
import https from "node:https";
|
||||||
|
import { Readable } from "node:stream";
|
||||||
|
|
||||||
|
type UpstreamFetchInit = {
|
||||||
|
method: string;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
body?: string;
|
||||||
|
skipTlsVerification?: boolean;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
};
|
||||||
|
|
||||||
|
const insecureHttpsAgent = new https.Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
keepAlive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export function aiGatewayUpstreamFetch(
|
||||||
|
url: string,
|
||||||
|
init: UpstreamFetchInit
|
||||||
|
): Promise<Response> {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
const isHttps = parsed.protocol === "https:";
|
||||||
|
const lib = isHttps ? https : http;
|
||||||
|
const agent =
|
||||||
|
isHttps && init.skipTlsVerification ? insecureHttpsAgent : undefined;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (init.signal?.aborted) {
|
||||||
|
reject(init.signal.reason ?? new Error("Request aborted"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = lib.request(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
method: init.method,
|
||||||
|
headers: init.headers,
|
||||||
|
agent
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
const headers = new Headers();
|
||||||
|
for (const [key, value] of Object.entries(res.headers)) {
|
||||||
|
if (value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const entry of value) {
|
||||||
|
headers.append(key, entry);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headers.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = Readable.toWeb(res) as ReadableStream<Uint8Array>;
|
||||||
|
resolve(
|
||||||
|
new Response(body, {
|
||||||
|
status: res.statusCode ?? 502,
|
||||||
|
statusText: res.statusMessage,
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on("error", reject);
|
||||||
|
|
||||||
|
if (init.signal) {
|
||||||
|
const onAbort = () => req.destroy(init.signal!.reason);
|
||||||
|
init.signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
req.on("close", () =>
|
||||||
|
init.signal!.removeEventListener("abort", onAbort)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (init.body !== undefined) {
|
||||||
|
req.write(init.body);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,721 @@
|
|||||||
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
aiModels,
|
||||||
|
aiProviders,
|
||||||
|
db,
|
||||||
|
resourceAiModels,
|
||||||
|
resourceAiProviders,
|
||||||
|
siteResourceAiModels,
|
||||||
|
siteResourceAiProviders,
|
||||||
|
type Transaction
|
||||||
|
} from "@server/db";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type DbOrTrx = Transaction | typeof db;
|
||||||
|
|
||||||
|
export const modelListTypeSchema = z.enum(["allow", "block"]);
|
||||||
|
|
||||||
|
export type ModelListType = z.infer<typeof modelListTypeSchema>;
|
||||||
|
|
||||||
|
export const accessModeSchema = z.enum(["inherit", "select"]);
|
||||||
|
|
||||||
|
export type AccessMode = z.infer<typeof accessModeSchema>;
|
||||||
|
|
||||||
|
export const resourceAiProviderAttachmentSchema = z.strictObject({
|
||||||
|
providerId: z.number().int().positive(),
|
||||||
|
accessMode: accessModeSchema.optional().default("inherit"),
|
||||||
|
enabled: z.boolean().optional().default(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResourceAiProviderInput = z.infer<
|
||||||
|
typeof resourceAiProviderAttachmentSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ResourceAiProviderAttachment = {
|
||||||
|
providerId: number;
|
||||||
|
accessMode: AccessMode;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resourceAiModelEntrySchema = z.strictObject({
|
||||||
|
modelId: z.number().int().positive(),
|
||||||
|
listType: modelListTypeSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResourceAiModelEntry = z.infer<typeof resourceAiModelEntrySchema>;
|
||||||
|
|
||||||
|
export type InferenceFieldsError = {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isInferenceFieldsError(
|
||||||
|
value: { error: string } | object
|
||||||
|
): value is InferenceFieldsError {
|
||||||
|
return "error" in value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve which allow/block patterns apply for an attachment.
|
||||||
|
* inherit → provider lists; select → resource-selected lists (replace).
|
||||||
|
*/
|
||||||
|
export function resolveEffectiveLists(input: {
|
||||||
|
accessMode: AccessMode;
|
||||||
|
providerAllows: string[];
|
||||||
|
providerBlocks: string[];
|
||||||
|
resourceAllows: string[];
|
||||||
|
resourceBlocks: string[];
|
||||||
|
}): { allows: string[]; blocks: string[] } {
|
||||||
|
if (input.accessMode === "select") {
|
||||||
|
return {
|
||||||
|
allows: input.resourceAllows,
|
||||||
|
blocks: input.resourceBlocks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
allows: input.providerAllows,
|
||||||
|
blocks: input.providerBlocks
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAttachments(
|
||||||
|
inputs: ResourceAiProviderInput[]
|
||||||
|
): ResourceAiProviderAttachment[] {
|
||||||
|
const byProviderId = new Map<
|
||||||
|
number,
|
||||||
|
{ accessMode: AccessMode; enabled: boolean }
|
||||||
|
>();
|
||||||
|
for (const input of inputs) {
|
||||||
|
byProviderId.set(input.providerId, {
|
||||||
|
accessMode: input.accessMode ?? "inherit",
|
||||||
|
enabled: input.enabled ?? true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [...byProviderId.entries()].map(
|
||||||
|
([providerId, { accessMode, enabled }]) => ({
|
||||||
|
providerId,
|
||||||
|
accessMode,
|
||||||
|
enabled
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate provider attachments for an org.
|
||||||
|
*/
|
||||||
|
export async function resolveProviderAttachments(input: {
|
||||||
|
orgId: string;
|
||||||
|
attachments: ResourceAiProviderInput[];
|
||||||
|
requireAtLeastOne: boolean;
|
||||||
|
}): Promise<ResourceAiProviderAttachment[] | InferenceFieldsError> {
|
||||||
|
const attachments = normalizeAttachments(input.attachments);
|
||||||
|
|
||||||
|
if (input.requireAtLeastOne && attachments.length === 0) {
|
||||||
|
return {
|
||||||
|
error: "At least one AI provider is required for inference-mode resources"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachments.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerIds = attachments.map((a) => a.providerId);
|
||||||
|
const providers = await db
|
||||||
|
.select({
|
||||||
|
providerId: aiProviders.providerId,
|
||||||
|
orgId: aiProviders.orgId,
|
||||||
|
enabled: aiProviders.enabled
|
||||||
|
})
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(aiProviders.providerId, providerIds),
|
||||||
|
eq(aiProviders.orgId, input.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (providers.length !== providerIds.length) {
|
||||||
|
return {
|
||||||
|
error: "One or more AI providers were not found in this organization"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabled = providers.find((p) => !p.enabled);
|
||||||
|
if (disabled) {
|
||||||
|
return {
|
||||||
|
error: `AI provider with ID ${disabled.providerId} is disabled`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertInferenceModeAllowsProviderFields(input: {
|
||||||
|
mode: string;
|
||||||
|
hasProviderAttachments: boolean;
|
||||||
|
}): Promise<InferenceFieldsError | null> {
|
||||||
|
if (input.mode === "inference") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (input.hasProviderAttachments) {
|
||||||
|
return {
|
||||||
|
error: "AI providers can only be attached to inference-mode resources"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach providers to a resource. Inherit attachments use the provider lists
|
||||||
|
* as-is (resource model rows for those providers are pruned). Select
|
||||||
|
* attachments keep resource-selected allow/block subsets.
|
||||||
|
*/
|
||||||
|
export async function setPublicResourceAiProviders(
|
||||||
|
resourceId: number,
|
||||||
|
attachments: ResourceAiProviderAttachment[],
|
||||||
|
trx: DbOrTrx = db
|
||||||
|
): Promise<void> {
|
||||||
|
await trx
|
||||||
|
.delete(resourceAiProviders)
|
||||||
|
.where(eq(resourceAiProviders.resourceId, resourceId));
|
||||||
|
|
||||||
|
if (attachments.length > 0) {
|
||||||
|
await trx.insert(resourceAiProviders).values(
|
||||||
|
attachments.map((a) => ({
|
||||||
|
resourceId,
|
||||||
|
providerId: a.providerId,
|
||||||
|
accessMode: a.accessMode,
|
||||||
|
enabled: a.enabled
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prunePublicResourceModelsToSelectProviders(
|
||||||
|
resourceId,
|
||||||
|
attachments,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setSiteResourceAiProviders(
|
||||||
|
siteResourceId: number,
|
||||||
|
attachments: ResourceAiProviderAttachment[],
|
||||||
|
trx: DbOrTrx = db
|
||||||
|
): Promise<void> {
|
||||||
|
await trx
|
||||||
|
.delete(siteResourceAiProviders)
|
||||||
|
.where(eq(siteResourceAiProviders.siteResourceId, siteResourceId));
|
||||||
|
|
||||||
|
if (attachments.length > 0) {
|
||||||
|
await trx.insert(siteResourceAiProviders).values(
|
||||||
|
attachments.map((a) => ({
|
||||||
|
siteResourceId,
|
||||||
|
providerId: a.providerId,
|
||||||
|
accessMode: a.accessMode,
|
||||||
|
enabled: a.enabled
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await pruneSiteResourceModelsToSelectProviders(
|
||||||
|
siteResourceId,
|
||||||
|
attachments,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep resource model rows only for providers in select mode.
|
||||||
|
*/
|
||||||
|
async function prunePublicResourceModelsToSelectProviders(
|
||||||
|
resourceId: number,
|
||||||
|
attachments: ResourceAiProviderAttachment[],
|
||||||
|
trx: DbOrTrx
|
||||||
|
): Promise<void> {
|
||||||
|
const selectProviderIds = attachments
|
||||||
|
.filter((a) => a.accessMode === "select")
|
||||||
|
.map((a) => a.providerId);
|
||||||
|
|
||||||
|
if (selectProviderIds.length === 0) {
|
||||||
|
await trx
|
||||||
|
.delete(resourceAiModels)
|
||||||
|
.where(eq(resourceAiModels.resourceId, resourceId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await trx
|
||||||
|
.select({
|
||||||
|
modelId: resourceAiModels.modelId,
|
||||||
|
providerId: aiModels.providerId
|
||||||
|
})
|
||||||
|
.from(resourceAiModels)
|
||||||
|
.innerJoin(aiModels, eq(resourceAiModels.modelId, aiModels.modelId))
|
||||||
|
.where(eq(resourceAiModels.resourceId, resourceId));
|
||||||
|
|
||||||
|
const allowed = new Set(selectProviderIds);
|
||||||
|
const toRemove = existing
|
||||||
|
.filter((row) => !allowed.has(row.providerId))
|
||||||
|
.map((row) => row.modelId);
|
||||||
|
|
||||||
|
if (toRemove.length > 0) {
|
||||||
|
await trx
|
||||||
|
.delete(resourceAiModels)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourceAiModels.resourceId, resourceId),
|
||||||
|
inArray(resourceAiModels.modelId, toRemove)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pruneSiteResourceModelsToSelectProviders(
|
||||||
|
siteResourceId: number,
|
||||||
|
attachments: ResourceAiProviderAttachment[],
|
||||||
|
trx: DbOrTrx
|
||||||
|
): Promise<void> {
|
||||||
|
const selectProviderIds = attachments
|
||||||
|
.filter((a) => a.accessMode === "select")
|
||||||
|
.map((a) => a.providerId);
|
||||||
|
|
||||||
|
if (selectProviderIds.length === 0) {
|
||||||
|
await trx
|
||||||
|
.delete(siteResourceAiModels)
|
||||||
|
.where(eq(siteResourceAiModels.siteResourceId, siteResourceId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await trx
|
||||||
|
.select({
|
||||||
|
modelId: siteResourceAiModels.modelId,
|
||||||
|
providerId: aiModels.providerId
|
||||||
|
})
|
||||||
|
.from(siteResourceAiModels)
|
||||||
|
.innerJoin(aiModels, eq(siteResourceAiModels.modelId, aiModels.modelId))
|
||||||
|
.where(eq(siteResourceAiModels.siteResourceId, siteResourceId));
|
||||||
|
|
||||||
|
const allowed = new Set(selectProviderIds);
|
||||||
|
const toRemove = existing
|
||||||
|
.filter((row) => !allowed.has(row.providerId))
|
||||||
|
.map((row) => row.modelId);
|
||||||
|
|
||||||
|
if (toRemove.length > 0) {
|
||||||
|
await trx
|
||||||
|
.delete(siteResourceAiModels)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResourceAiModels.siteResourceId, siteResourceId),
|
||||||
|
inArray(siteResourceAiModels.modelId, toRemove)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearPublicResourceAiConfig(
|
||||||
|
resourceId: number,
|
||||||
|
trx: DbOrTrx = db
|
||||||
|
): Promise<void> {
|
||||||
|
await trx
|
||||||
|
.delete(resourceAiModels)
|
||||||
|
.where(eq(resourceAiModels.resourceId, resourceId));
|
||||||
|
await trx
|
||||||
|
.delete(resourceAiProviders)
|
||||||
|
.where(eq(resourceAiProviders.resourceId, resourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearSiteResourceAiConfig(
|
||||||
|
siteResourceId: number,
|
||||||
|
trx: DbOrTrx = db
|
||||||
|
): Promise<void> {
|
||||||
|
await trx
|
||||||
|
.delete(siteResourceAiModels)
|
||||||
|
.where(eq(siteResourceAiModels.siteResourceId, siteResourceId));
|
||||||
|
await trx
|
||||||
|
.delete(siteResourceAiProviders)
|
||||||
|
.where(eq(siteResourceAiProviders.siteResourceId, siteResourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listPublicResourceAiProviders(resourceId: number) {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
providerId: resourceAiProviders.providerId,
|
||||||
|
niceId: aiProviders.niceId,
|
||||||
|
name: aiProviders.name,
|
||||||
|
type: aiProviders.type,
|
||||||
|
enabled: resourceAiProviders.enabled,
|
||||||
|
providerEnabled: aiProviders.enabled,
|
||||||
|
accessMode: resourceAiProviders.accessMode
|
||||||
|
})
|
||||||
|
.from(resourceAiProviders)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(resourceAiProviders.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(eq(resourceAiProviders.resourceId, resourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listSiteResourceAiProviders(siteResourceId: number) {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
providerId: siteResourceAiProviders.providerId,
|
||||||
|
niceId: aiProviders.niceId,
|
||||||
|
name: aiProviders.name,
|
||||||
|
type: aiProviders.type,
|
||||||
|
enabled: siteResourceAiProviders.enabled,
|
||||||
|
providerEnabled: aiProviders.enabled,
|
||||||
|
accessMode: siteResourceAiProviders.accessMode
|
||||||
|
})
|
||||||
|
.from(siteResourceAiProviders)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(siteResourceAiProviders.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(eq(siteResourceAiProviders.siteResourceId, siteResourceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EffectiveAllowModel = {
|
||||||
|
modelId: number;
|
||||||
|
modelKey: string;
|
||||||
|
name: string;
|
||||||
|
providerId: number;
|
||||||
|
providerName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function listEffectiveAllowModels(options: {
|
||||||
|
resourceId?: number;
|
||||||
|
siteResourceId?: number;
|
||||||
|
}): Promise<EffectiveAllowModel[]> {
|
||||||
|
if (
|
||||||
|
options.resourceId === undefined &&
|
||||||
|
options.siteResourceId === undefined
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachments =
|
||||||
|
options.resourceId !== undefined
|
||||||
|
? await listPublicResourceAiProviders(options.resourceId)
|
||||||
|
: await listSiteResourceAiProviders(options.siteResourceId!);
|
||||||
|
|
||||||
|
const activeAttachments = attachments.filter(
|
||||||
|
(a) => a.enabled && a.providerEnabled
|
||||||
|
);
|
||||||
|
if (activeAttachments.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const inheritProviderIds = activeAttachments
|
||||||
|
.filter((a) => a.accessMode === "inherit")
|
||||||
|
.map((a) => a.providerId);
|
||||||
|
const selectProviderIds = activeAttachments
|
||||||
|
.filter((a) => a.accessMode === "select")
|
||||||
|
.map((a) => a.providerId);
|
||||||
|
|
||||||
|
const providerNameById = new Map(
|
||||||
|
activeAttachments.map((a) => [a.providerId, a.name] as const)
|
||||||
|
);
|
||||||
|
|
||||||
|
const models: EffectiveAllowModel[] = [];
|
||||||
|
|
||||||
|
if (inheritProviderIds.length > 0) {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
modelId: aiModels.modelId,
|
||||||
|
modelKey: aiModels.modelKey,
|
||||||
|
name: aiModels.name,
|
||||||
|
providerId: aiModels.providerId
|
||||||
|
})
|
||||||
|
.from(aiModels)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(aiModels.providerId, inheritProviderIds),
|
||||||
|
eq(aiModels.enabled, true),
|
||||||
|
eq(aiModels.listType, "allow")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (const row of rows) {
|
||||||
|
models.push({
|
||||||
|
...row,
|
||||||
|
providerName: providerNameById.get(row.providerId) ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectProviderIds.length > 0) {
|
||||||
|
if (options.resourceId !== undefined) {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
modelId: aiModels.modelId,
|
||||||
|
modelKey: aiModels.modelKey,
|
||||||
|
name: aiModels.name,
|
||||||
|
providerId: aiModels.providerId
|
||||||
|
})
|
||||||
|
.from(resourceAiModels)
|
||||||
|
.innerJoin(
|
||||||
|
aiModels,
|
||||||
|
eq(resourceAiModels.modelId, aiModels.modelId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourceAiModels.resourceId, options.resourceId),
|
||||||
|
inArray(aiModels.providerId, selectProviderIds),
|
||||||
|
eq(resourceAiModels.listType, "allow"),
|
||||||
|
eq(aiModels.enabled, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (const row of rows) {
|
||||||
|
models.push({
|
||||||
|
...row,
|
||||||
|
providerName: providerNameById.get(row.providerId) ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (options.siteResourceId !== undefined) {
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
modelId: aiModels.modelId,
|
||||||
|
modelKey: aiModels.modelKey,
|
||||||
|
name: aiModels.name,
|
||||||
|
providerId: aiModels.providerId
|
||||||
|
})
|
||||||
|
.from(siteResourceAiModels)
|
||||||
|
.innerJoin(
|
||||||
|
aiModels,
|
||||||
|
eq(siteResourceAiModels.modelId, aiModels.modelId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
siteResourceAiModels.siteResourceId,
|
||||||
|
options.siteResourceId
|
||||||
|
),
|
||||||
|
inArray(aiModels.providerId, selectProviderIds),
|
||||||
|
eq(siteResourceAiModels.listType, "allow"),
|
||||||
|
eq(aiModels.enabled, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (const row of rows) {
|
||||||
|
models.push({
|
||||||
|
...row,
|
||||||
|
providerName: providerNameById.get(row.providerId) ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
models.sort((a, b) => {
|
||||||
|
const byProvider = a.providerName.localeCompare(
|
||||||
|
b.providerName,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
sensitivity: "base"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (byProvider !== 0) {
|
||||||
|
return byProvider;
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
||||||
|
});
|
||||||
|
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model list APIs require an inference resource with at least one select-mode
|
||||||
|
* attached provider.
|
||||||
|
*/
|
||||||
|
export async function assertPublicModelListApiEligible(resource: {
|
||||||
|
resourceId: number;
|
||||||
|
mode: string;
|
||||||
|
}): Promise<string | null> {
|
||||||
|
if (resource.mode !== "inference") {
|
||||||
|
return "AI model lists are only supported on inference-mode resources";
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ providerId: resourceAiProviders.providerId })
|
||||||
|
.from(resourceAiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourceAiProviders.resourceId, resource.resourceId),
|
||||||
|
eq(resourceAiProviders.accessMode, "select")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return "Set at least one attached AI provider to select mode before managing model lists";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertSiteModelListApiEligible(siteResource: {
|
||||||
|
siteResourceId: number;
|
||||||
|
mode: string;
|
||||||
|
}): Promise<string | null> {
|
||||||
|
if (siteResource.mode !== "inference") {
|
||||||
|
return "AI model lists are only supported on inference-mode resources";
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({ providerId: siteResourceAiProviders.providerId })
|
||||||
|
.from(siteResourceAiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
siteResourceAiProviders.siteResourceId,
|
||||||
|
siteResource.siteResourceId
|
||||||
|
),
|
||||||
|
eq(siteResourceAiProviders.accessMode, "select")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return "Set at least one attached AI provider to select mode before managing model lists";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource model entries must belong to select-mode attached providers, and
|
||||||
|
* listType must match the provider catalog entry (allow→allow, block→block).
|
||||||
|
*/
|
||||||
|
export async function assertPublicResourceModelEntriesValid(input: {
|
||||||
|
orgId: string;
|
||||||
|
resourceId: number;
|
||||||
|
models: ResourceAiModelEntry[];
|
||||||
|
}): Promise<string | null> {
|
||||||
|
const uniqueModels = dedupeModelEntries(input.models);
|
||||||
|
if (uniqueModels.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachments = await db
|
||||||
|
.select({
|
||||||
|
providerId: resourceAiProviders.providerId,
|
||||||
|
accessMode: resourceAiProviders.accessMode,
|
||||||
|
enabled: resourceAiProviders.enabled
|
||||||
|
})
|
||||||
|
.from(resourceAiProviders)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(resourceAiProviders.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resourceAiProviders.resourceId, input.resourceId),
|
||||||
|
eq(aiProviders.orgId, input.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return assertModelEntriesValid({
|
||||||
|
orgId: input.orgId,
|
||||||
|
modelEntries: uniqueModels,
|
||||||
|
attachments,
|
||||||
|
resourceLabel: "resource"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertSiteResourceModelEntriesValid(input: {
|
||||||
|
orgId: string;
|
||||||
|
siteResourceId: number;
|
||||||
|
models: ResourceAiModelEntry[];
|
||||||
|
}): Promise<string | null> {
|
||||||
|
const uniqueModels = dedupeModelEntries(input.models);
|
||||||
|
if (uniqueModels.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachments = await db
|
||||||
|
.select({
|
||||||
|
providerId: siteResourceAiProviders.providerId,
|
||||||
|
accessMode: siteResourceAiProviders.accessMode,
|
||||||
|
enabled: siteResourceAiProviders.enabled
|
||||||
|
})
|
||||||
|
.from(siteResourceAiProviders)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(siteResourceAiProviders.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
siteResourceAiProviders.siteResourceId,
|
||||||
|
input.siteResourceId
|
||||||
|
),
|
||||||
|
eq(aiProviders.orgId, input.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return assertModelEntriesValid({
|
||||||
|
orgId: input.orgId,
|
||||||
|
modelEntries: uniqueModels,
|
||||||
|
attachments,
|
||||||
|
resourceLabel: "site resource"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeModelEntries(
|
||||||
|
models: ResourceAiModelEntry[]
|
||||||
|
): ResourceAiModelEntry[] {
|
||||||
|
const byModelId = new Map(
|
||||||
|
models.map((m) => [m.modelId, m.listType] as const)
|
||||||
|
);
|
||||||
|
return [...byModelId.entries()].map(([modelId, listType]) => ({
|
||||||
|
modelId,
|
||||||
|
listType
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertModelEntriesValid(input: {
|
||||||
|
orgId: string;
|
||||||
|
modelEntries: ResourceAiModelEntry[];
|
||||||
|
attachments: ResourceAiProviderAttachment[];
|
||||||
|
resourceLabel: string;
|
||||||
|
}): Promise<string | null> {
|
||||||
|
const selectProviderIds = input.attachments
|
||||||
|
.filter((a) => a.accessMode === "select")
|
||||||
|
.map((a) => a.providerId);
|
||||||
|
|
||||||
|
if (selectProviderIds.length === 0) {
|
||||||
|
return "Set at least one attached AI provider to select mode before managing model lists";
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelIds = input.modelEntries.map((m) => m.modelId);
|
||||||
|
const catalogRows = await db
|
||||||
|
.select({
|
||||||
|
modelId: aiModels.modelId,
|
||||||
|
listType: aiModels.listType,
|
||||||
|
providerId: aiModels.providerId,
|
||||||
|
enabled: aiModels.enabled
|
||||||
|
})
|
||||||
|
.from(aiModels)
|
||||||
|
.innerJoin(aiProviders, eq(aiModels.providerId, aiProviders.providerId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(aiModels.modelId, modelIds),
|
||||||
|
inArray(aiModels.providerId, selectProviderIds),
|
||||||
|
eq(aiProviders.orgId, input.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (catalogRows.length !== modelIds.length) {
|
||||||
|
return `One or more model IDs do not exist or do not belong to a select-mode provider on this ${input.resourceLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalogById = new Map(catalogRows.map((row) => [row.modelId, row]));
|
||||||
|
for (const entry of input.modelEntries) {
|
||||||
|
const catalog = catalogById.get(entry.modelId);
|
||||||
|
if (!catalog) {
|
||||||
|
return `One or more model IDs do not exist or do not belong to a select-mode provider on this ${input.resourceLabel}`;
|
||||||
|
}
|
||||||
|
if (catalog.listType !== entry.listType) {
|
||||||
|
return `Model ${entry.modelId} must use listType "${catalog.listType}" to match the provider catalog entry`;
|
||||||
|
}
|
||||||
|
if (!catalog.enabled) {
|
||||||
|
return `Model ${entry.modelId} is disabled on its provider`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,538 @@
|
|||||||
|
import type { AiCapability } from "@server/lib/aiCapabilities";
|
||||||
|
import { sseDataFrames, tryParseJson } from "@server/lib/aiUsageExtraction";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
// Uniform, capability-agnostic representation of a chat message, used so
|
||||||
|
// the AI session log can be searched/displayed the same way regardless of
|
||||||
|
// which provider/capability produced it. Content is flattened to plain text
|
||||||
|
// - non-text parts (images, tool calls/results) are rendered as readable
|
||||||
|
// placeholders rather than preserved as structured data, which is enough for
|
||||||
|
// a transcript-style replay view without a per-capability renderer.
|
||||||
|
export type NormalizedRole = "system" | "user" | "assistant" | "tool";
|
||||||
|
|
||||||
|
export type NormalizedAiMessage = {
|
||||||
|
role: NormalizedRole;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeRole(role: unknown): NormalizedRole {
|
||||||
|
if (
|
||||||
|
role === "system" ||
|
||||||
|
role === "user" ||
|
||||||
|
role === "assistant" ||
|
||||||
|
role === "tool"
|
||||||
|
) {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
if (role === "model") return "assistant"; // Gemini
|
||||||
|
if (role === "function") return "tool"; // OpenAI legacy function role
|
||||||
|
return "user";
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeJsonStringify(value: unknown): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value ?? {});
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattens one message "part"/"block" (OpenAI content parts, Anthropic
|
||||||
|
* content blocks, Gemini parts, Bedrock converse content blocks - they all
|
||||||
|
* follow the same rough shape) into readable text.
|
||||||
|
*/
|
||||||
|
function flattenContentPart(part: unknown): string {
|
||||||
|
if (typeof part === "string") return part;
|
||||||
|
if (part == null || typeof part !== "object") return "";
|
||||||
|
const p = part as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (typeof p.text === "string") return p.text;
|
||||||
|
|
||||||
|
if (
|
||||||
|
p.type === "image_url" ||
|
||||||
|
p.type === "image" ||
|
||||||
|
p.type === "input_image" ||
|
||||||
|
p.type === "output_image" ||
|
||||||
|
"inlineData" in p
|
||||||
|
) {
|
||||||
|
return "[image]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anthropic-style tool_use / tool_result blocks
|
||||||
|
if (p.type === "tool_use") {
|
||||||
|
const name = typeof p.name === "string" ? p.name : "tool";
|
||||||
|
return `[tool_call: ${name}(${safeJsonStringify(p.input)})]`;
|
||||||
|
}
|
||||||
|
if (p.type === "tool_result") {
|
||||||
|
const content = p.content;
|
||||||
|
const text =
|
||||||
|
typeof content === "string"
|
||||||
|
? content
|
||||||
|
: Array.isArray(content)
|
||||||
|
? flattenContentParts(content)
|
||||||
|
: "";
|
||||||
|
return `[tool_result: ${text}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemini-style functionCall / functionResponse parts
|
||||||
|
if (p.functionCall && typeof p.functionCall === "object") {
|
||||||
|
const fc = p.functionCall as Record<string, unknown>;
|
||||||
|
return `[tool_call: ${fc.name}(${safeJsonStringify(fc.args)})]`;
|
||||||
|
}
|
||||||
|
if (p.functionResponse && typeof p.functionResponse === "object") {
|
||||||
|
const fr = p.functionResponse as Record<string, unknown>;
|
||||||
|
return `[tool_result: ${fr.name}(${safeJsonStringify(fr.response)})]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bedrock converse-style toolUse / toolResult content blocks
|
||||||
|
if (p.toolUse && typeof p.toolUse === "object") {
|
||||||
|
const tu = p.toolUse as Record<string, unknown>;
|
||||||
|
return `[tool_call: ${tu.name}(${safeJsonStringify(tu.input)})]`;
|
||||||
|
}
|
||||||
|
if (p.toolResult && typeof p.toolResult === "object") {
|
||||||
|
const tr = p.toolResult as Record<string, unknown>;
|
||||||
|
const content = tr.content;
|
||||||
|
const text = Array.isArray(content) ? flattenContentParts(content) : "";
|
||||||
|
return `[tool_result: ${text}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenContentParts(parts: unknown[]): string {
|
||||||
|
return parts.map(flattenContentPart).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenContent(content: unknown): string {
|
||||||
|
if (typeof content === "string") return content;
|
||||||
|
if (Array.isArray(content)) return flattenContentParts(content);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort scan for every `"text":"..."` JSON string value in raw text,
|
||||||
|
* concatenated in order. Fallback for streaming formats we can't fully parse
|
||||||
|
* as JSON/SSE (Gemini's array-JSON stream, Bedrock's binary event-stream
|
||||||
|
* framing) - same spirit as aiUsageExtraction's scanNumericFields.
|
||||||
|
*/
|
||||||
|
function scanTextFragments(text: string): string {
|
||||||
|
const out: string[] = [];
|
||||||
|
const re = /"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = re.exec(text)) !== null) {
|
||||||
|
try {
|
||||||
|
out.push(JSON.parse(`"${match[1]}"`));
|
||||||
|
} catch {
|
||||||
|
out.push(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Request (input) normalizers - operate on the already-parsed outbound body.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function normalizeOpenAiChatRequest(body: any): NormalizedAiMessage[] {
|
||||||
|
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
||||||
|
return messages.map((m: any) => ({
|
||||||
|
role: normalizeRole(m?.role),
|
||||||
|
content: flattenContent(m?.content)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOpenAiResponsesRequest(body: any): NormalizedAiMessage[] {
|
||||||
|
const out: NormalizedAiMessage[] = [];
|
||||||
|
if (typeof body?.instructions === "string" && body.instructions) {
|
||||||
|
out.push({ role: "system", content: body.instructions });
|
||||||
|
}
|
||||||
|
const input = body?.input;
|
||||||
|
if (typeof input === "string") {
|
||||||
|
out.push({ role: "user", content: input });
|
||||||
|
} else if (Array.isArray(input)) {
|
||||||
|
for (const item of input) {
|
||||||
|
if (item?.role) {
|
||||||
|
out.push({
|
||||||
|
role: normalizeRole(item.role),
|
||||||
|
content: flattenContent(item.content)
|
||||||
|
});
|
||||||
|
} else if (typeof item?.type === "string") {
|
||||||
|
out.push({ role: "tool", content: `[${item.type}]` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAnthropicRequest(body: any): NormalizedAiMessage[] {
|
||||||
|
const out: NormalizedAiMessage[] = [];
|
||||||
|
if (body?.system) {
|
||||||
|
const sys = flattenContent(body.system);
|
||||||
|
if (sys) out.push({ role: "system", content: sys });
|
||||||
|
}
|
||||||
|
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
||||||
|
for (const m of messages) {
|
||||||
|
out.push({
|
||||||
|
role: normalizeRole(m?.role),
|
||||||
|
content: flattenContent(m?.content)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGeminiRequest(body: any): NormalizedAiMessage[] {
|
||||||
|
const out: NormalizedAiMessage[] = [];
|
||||||
|
const sysParts = body?.systemInstruction?.parts;
|
||||||
|
if (Array.isArray(sysParts)) {
|
||||||
|
const text = flattenContentParts(sysParts);
|
||||||
|
if (text) out.push({ role: "system", content: text });
|
||||||
|
}
|
||||||
|
const contents = Array.isArray(body?.contents) ? body.contents : [];
|
||||||
|
for (const c of contents) {
|
||||||
|
out.push({
|
||||||
|
role: normalizeRole(c?.role),
|
||||||
|
content: Array.isArray(c?.parts) ? flattenContentParts(c.parts) : ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBedrockConverseRequest(body: any): NormalizedAiMessage[] {
|
||||||
|
const out: NormalizedAiMessage[] = [];
|
||||||
|
if (Array.isArray(body?.system)) {
|
||||||
|
const text = flattenContentParts(body.system);
|
||||||
|
if (text) out.push({ role: "system", content: text });
|
||||||
|
}
|
||||||
|
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
||||||
|
for (const m of messages) {
|
||||||
|
out.push({
|
||||||
|
role: normalizeRole(m?.role),
|
||||||
|
content: Array.isArray(m?.content)
|
||||||
|
? flattenContentParts(m.content)
|
||||||
|
: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* bedrock_model_invoke and google_raw_predict are passthroughs - the body
|
||||||
|
* shape depends entirely on the underlying model, not the capability. Try
|
||||||
|
* the two shapes we're most likely to see (Anthropic Claude, then plain
|
||||||
|
* OpenAI-style) and give up otherwise, same fallback spirit
|
||||||
|
* aiUsageExtraction.ts uses for these two capabilities' usage extraction.
|
||||||
|
*/
|
||||||
|
function normalizeBestEffortRequest(body: any): NormalizedAiMessage[] | null {
|
||||||
|
if (!Array.isArray(body?.messages)) return null;
|
||||||
|
const looksAnthropicShaped = body.messages.some((m: any) =>
|
||||||
|
Array.isArray(m?.content)
|
||||||
|
);
|
||||||
|
return looksAnthropicShaped
|
||||||
|
? normalizeAnthropicRequest(body)
|
||||||
|
: normalizeOpenAiChatRequest(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Response (output) normalizers - operate on the raw response text, which
|
||||||
|
// may be a single JSON document (non-streaming) or provider-framed streaming
|
||||||
|
// text (SSE `data:` frames, a JSON-array stream, or binary event-stream
|
||||||
|
// framing with JSON payloads embedded in it).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function normalizeOpenAiChatResponse(
|
||||||
|
text: string,
|
||||||
|
isStream: boolean
|
||||||
|
): NormalizedAiMessage[] | null {
|
||||||
|
if (isStream) {
|
||||||
|
let role: unknown = "assistant";
|
||||||
|
let content = "";
|
||||||
|
let found = false;
|
||||||
|
for (const frame of sseDataFrames(text)) {
|
||||||
|
const delta = tryParseJson(frame)?.choices?.[0]?.delta;
|
||||||
|
if (!delta) continue;
|
||||||
|
found = true;
|
||||||
|
if (typeof delta.role === "string") role = delta.role;
|
||||||
|
if (typeof delta.content === "string") content += delta.content;
|
||||||
|
}
|
||||||
|
return found ? [{ role: normalizeRole(role), content }] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = tryParseJson(text)?.choices?.[0]?.message;
|
||||||
|
if (!message) return null;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
role: normalizeRole(message.role),
|
||||||
|
content: flattenContent(message.content)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractOpenAiResponsesOutputText(response: any): string | null {
|
||||||
|
if (typeof response?.output_text === "string") return response.output_text;
|
||||||
|
const output = Array.isArray(response?.output) ? response.output : [];
|
||||||
|
const pieces: string[] = [];
|
||||||
|
for (const item of output) {
|
||||||
|
if (item?.type === "message" && Array.isArray(item.content)) {
|
||||||
|
pieces.push(flattenContentParts(item.content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pieces.length > 0 ? pieces.join("") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOpenAiResponsesResponse(
|
||||||
|
text: string,
|
||||||
|
isStream: boolean
|
||||||
|
): NormalizedAiMessage[] | null {
|
||||||
|
if (isStream) {
|
||||||
|
let content = "";
|
||||||
|
let found = false;
|
||||||
|
for (const frame of sseDataFrames(text)) {
|
||||||
|
const parsed = tryParseJson(frame);
|
||||||
|
if (!parsed) continue;
|
||||||
|
if (
|
||||||
|
parsed.type === "response.output_text.delta" &&
|
||||||
|
typeof parsed.delta === "string"
|
||||||
|
) {
|
||||||
|
content += parsed.delta;
|
||||||
|
found = true;
|
||||||
|
} else if (
|
||||||
|
parsed.type === "response.completed" &&
|
||||||
|
parsed.response
|
||||||
|
) {
|
||||||
|
const outputText = extractOpenAiResponsesOutputText(
|
||||||
|
parsed.response
|
||||||
|
);
|
||||||
|
if (outputText != null) {
|
||||||
|
content = outputText;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found ? [{ role: "assistant", content }] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = tryParseJson(text);
|
||||||
|
const outputText = extractOpenAiResponsesOutputText(
|
||||||
|
parsed?.response ?? parsed
|
||||||
|
);
|
||||||
|
return outputText != null
|
||||||
|
? [{ role: "assistant", content: outputText }]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAnthropicResponse(
|
||||||
|
text: string,
|
||||||
|
isStream: boolean
|
||||||
|
): NormalizedAiMessage[] | null {
|
||||||
|
if (isStream) {
|
||||||
|
let role: unknown = "assistant";
|
||||||
|
let content = "";
|
||||||
|
let found = false;
|
||||||
|
for (const frame of sseDataFrames(text)) {
|
||||||
|
const parsed = tryParseJson(frame);
|
||||||
|
if (!parsed) continue;
|
||||||
|
if (parsed.type === "message_start" && parsed.message?.role) {
|
||||||
|
role = parsed.message.role;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parsed.type === "content_block_start" &&
|
||||||
|
parsed.content_block?.type === "tool_use"
|
||||||
|
) {
|
||||||
|
const name = parsed.content_block.name ?? "tool";
|
||||||
|
content += `[tool_call: ${name}]`;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parsed.type === "content_block_delta" &&
|
||||||
|
typeof parsed.delta?.text === "string"
|
||||||
|
) {
|
||||||
|
content += parsed.delta.text;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found ? [{ role: normalizeRole(role), content }] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = tryParseJson(text);
|
||||||
|
if (!parsed || !Array.isArray(parsed.content)) return null;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
role: normalizeRole(parsed.role ?? "assistant"),
|
||||||
|
content: flattenContentParts(parsed.content)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function geminiCandidateParts(node: any): string {
|
||||||
|
const parts = node?.candidates?.[0]?.content?.parts;
|
||||||
|
return Array.isArray(parts) ? flattenContentParts(parts) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGeminiResponse(
|
||||||
|
text: string,
|
||||||
|
_isStream: boolean
|
||||||
|
): NormalizedAiMessage[] | null {
|
||||||
|
const frames = sseDataFrames(text);
|
||||||
|
let content = "";
|
||||||
|
let role: unknown = "model";
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
if (frames.length > 0) {
|
||||||
|
for (const frame of frames) {
|
||||||
|
const parsed = tryParseJson(frame);
|
||||||
|
const piece = geminiCandidateParts(parsed);
|
||||||
|
if (piece) {
|
||||||
|
content += piece;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
const r = parsed?.candidates?.[0]?.content?.role;
|
||||||
|
if (r) role = r;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const parsed = tryParseJson(text);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
for (const chunk of parsed) {
|
||||||
|
const piece = geminiCandidateParts(chunk);
|
||||||
|
if (piece) {
|
||||||
|
content += piece;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
const r = chunk?.candidates?.[0]?.content?.role;
|
||||||
|
if (r) role = r;
|
||||||
|
}
|
||||||
|
} else if (parsed) {
|
||||||
|
const piece = geminiCandidateParts(parsed);
|
||||||
|
if (piece) {
|
||||||
|
content = piece;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
const r = parsed?.candidates?.[0]?.content?.role;
|
||||||
|
if (r) role = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
const scanned = scanTextFragments(text);
|
||||||
|
return scanned
|
||||||
|
? [{ role: normalizeRole(role), content: scanned }]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
return [{ role: normalizeRole(role), content }];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBedrockConverseResponse(
|
||||||
|
text: string,
|
||||||
|
isStream: boolean
|
||||||
|
): NormalizedAiMessage[] | null {
|
||||||
|
if (!isStream) {
|
||||||
|
const message = tryParseJson(text)?.output?.message;
|
||||||
|
if (!message) return null;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
role: normalizeRole(message.role ?? "assistant"),
|
||||||
|
content: Array.isArray(message.content)
|
||||||
|
? flattenContentParts(message.content)
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// converse-stream uses AWS's binary event-stream framing, but the JSON
|
||||||
|
// payload of each event survives intact inside it (same assumption
|
||||||
|
// aiUsageExtraction.ts makes for usage) - scan for the text pieces.
|
||||||
|
const scanned = scanTextFragments(text);
|
||||||
|
return scanned ? [{ role: "assistant", content: scanned }] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBedrockModelInvokeResponse(
|
||||||
|
text: string,
|
||||||
|
isStream: boolean
|
||||||
|
): NormalizedAiMessage[] | null {
|
||||||
|
const anthropicStyle = normalizeAnthropicResponse(text, isStream);
|
||||||
|
if (anthropicStyle) return anthropicStyle;
|
||||||
|
const scanned = scanTextFragments(text);
|
||||||
|
return scanned ? [{ role: "assistant", content: scanned }] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGoogleRawPredictResponse(
|
||||||
|
text: string,
|
||||||
|
isStream: boolean
|
||||||
|
): NormalizedAiMessage[] | null {
|
||||||
|
const anthropicStyle = normalizeAnthropicResponse(text, isStream);
|
||||||
|
if (anthropicStyle) return anthropicStyle;
|
||||||
|
const scanned = scanTextFragments(text);
|
||||||
|
return scanned ? [{ role: "assistant", content: scanned }] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REQUEST_NORMALIZERS: Record<
|
||||||
|
AiCapability,
|
||||||
|
(body: any) => NormalizedAiMessage[] | null
|
||||||
|
> = {
|
||||||
|
openai_chat: normalizeOpenAiChatRequest,
|
||||||
|
openai_responses: normalizeOpenAiResponsesRequest,
|
||||||
|
anthropic_messages: normalizeAnthropicRequest,
|
||||||
|
gemini_generate_content: normalizeGeminiRequest,
|
||||||
|
google_generate_content: normalizeGeminiRequest,
|
||||||
|
google_raw_predict: normalizeBestEffortRequest,
|
||||||
|
bedrock_model_invoke: normalizeBestEffortRequest,
|
||||||
|
bedrock_converse: normalizeBedrockConverseRequest
|
||||||
|
};
|
||||||
|
|
||||||
|
const RESPONSE_NORMALIZERS: Record<
|
||||||
|
AiCapability,
|
||||||
|
(text: string, isStream: boolean) => NormalizedAiMessage[] | null
|
||||||
|
> = {
|
||||||
|
openai_chat: normalizeOpenAiChatResponse,
|
||||||
|
openai_responses: normalizeOpenAiResponsesResponse,
|
||||||
|
anthropic_messages: normalizeAnthropicResponse,
|
||||||
|
gemini_generate_content: normalizeGeminiResponse,
|
||||||
|
google_generate_content: normalizeGeminiResponse,
|
||||||
|
google_raw_predict: normalizeGoogleRawPredictResponse,
|
||||||
|
bedrock_model_invoke: normalizeBedrockModelInvokeResponse,
|
||||||
|
bedrock_converse: normalizeBedrockConverseResponse
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes an outbound AI gateway request body into a uniform message
|
||||||
|
* transcript, regardless of capability/provider. Returns null if the body
|
||||||
|
* doesn't contain any recognizable messages (or parsing failed) - callers
|
||||||
|
* should fall back to showing the raw request body.
|
||||||
|
*/
|
||||||
|
export function normalizeAiRequest(
|
||||||
|
capability: AiCapability,
|
||||||
|
body: unknown
|
||||||
|
): NormalizedAiMessage[] | null {
|
||||||
|
try {
|
||||||
|
const result = REQUEST_NORMALIZERS[capability](body);
|
||||||
|
return result && result.length > 0 ? result : null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug("Failed to normalize AI request messages", {
|
||||||
|
capability,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a completed (non-streaming or fully-accumulated streaming) AI
|
||||||
|
* gateway response into a uniform message transcript. Returns null if
|
||||||
|
* nothing recognizable could be extracted - callers should fall back to
|
||||||
|
* showing the raw response body.
|
||||||
|
*/
|
||||||
|
export function normalizeAiResponse(
|
||||||
|
capability: AiCapability,
|
||||||
|
responseText: string,
|
||||||
|
isStream: boolean
|
||||||
|
): NormalizedAiMessage[] | null {
|
||||||
|
try {
|
||||||
|
const result = RESPONSE_NORMALIZERS[capability](responseText, isStream);
|
||||||
|
return result && result.length > 0 ? result : null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug("Failed to normalize AI response messages", {
|
||||||
|
capability,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import axios from "axios";
|
||||||
|
import { z } from "zod";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import type { AiProviderType } from "@server/lib/aiProviderDefaults";
|
||||||
|
|
||||||
|
export const CATALOG_PROVIDERS = [
|
||||||
|
"openai",
|
||||||
|
"anthropic",
|
||||||
|
"gemini",
|
||||||
|
"vertex",
|
||||||
|
"azure",
|
||||||
|
"bedrock"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type CatalogProvider = (typeof CATALOG_PROVIDERS)[number];
|
||||||
|
|
||||||
|
const CATALOG_PROVIDER_SET = new Set<string>(CATALOG_PROVIDERS);
|
||||||
|
|
||||||
|
// Each of our provider types maps to at most one catalog provider. Provider
|
||||||
|
// types that proxy arbitrary underlying models (openRouter, vercelAiGateway,
|
||||||
|
// custom) have no mapping.
|
||||||
|
const PROVIDER_CATALOG_MAP: Record<
|
||||||
|
Exclude<AiProviderType, "custom">,
|
||||||
|
CatalogProvider | null
|
||||||
|
> = {
|
||||||
|
openai: "openai",
|
||||||
|
anthropic: "anthropic",
|
||||||
|
googleGemini: "gemini",
|
||||||
|
vertexAi: "vertex",
|
||||||
|
bedrock: "bedrock",
|
||||||
|
microsoftFoundry: "azure",
|
||||||
|
openRouter: null,
|
||||||
|
vercelAiGateway: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getCatalogProviderForType(
|
||||||
|
type: AiProviderType
|
||||||
|
): CatalogProvider | null {
|
||||||
|
if (type === "custom") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return PROVIDER_CATALOG_MAP[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AiModelCatalogEntry = {
|
||||||
|
provider: CatalogProvider;
|
||||||
|
model: string;
|
||||||
|
pricing: {
|
||||||
|
in: number | null;
|
||||||
|
out: number | null;
|
||||||
|
cache: number | null;
|
||||||
|
reasoning: number | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const catalogEntrySchema = z.object({
|
||||||
|
model: z.string(),
|
||||||
|
provider: z.string(),
|
||||||
|
pricing: z
|
||||||
|
.object({
|
||||||
|
in: z.number().nullable().optional(),
|
||||||
|
out: z.number().nullable().optional(),
|
||||||
|
cache: z.number().nullable().optional(),
|
||||||
|
reasoning: z.number().nullable().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const catalogFileSchema = z.object({
|
||||||
|
data: z.array(catalogEntrySchema).optional().default([])
|
||||||
|
});
|
||||||
|
|
||||||
|
type RawCatalogEntry = z.infer<typeof catalogEntrySchema>;
|
||||||
|
|
||||||
|
function normalizeCatalogProvider(raw: string): CatalogProvider | null {
|
||||||
|
if (CATALOG_PROVIDER_SET.has(raw)) {
|
||||||
|
return raw as CatalogProvider;
|
||||||
|
}
|
||||||
|
if (raw.startsWith("bedrock")) {
|
||||||
|
return "bedrock";
|
||||||
|
}
|
||||||
|
if (raw.startsWith("vertex")) {
|
||||||
|
return "vertex";
|
||||||
|
}
|
||||||
|
if (raw.startsWith("azure")) {
|
||||||
|
return "azure";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEntry(raw: RawCatalogEntry): AiModelCatalogEntry | null {
|
||||||
|
const provider = normalizeCatalogProvider(raw.provider);
|
||||||
|
if (!provider) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!raw.model) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider,
|
||||||
|
model: raw.model,
|
||||||
|
pricing: {
|
||||||
|
in: raw.pricing?.in ?? null,
|
||||||
|
out: raw.pricing?.out ?? null,
|
||||||
|
cache: raw.pricing?.cache ?? null,
|
||||||
|
reasoning: raw.pricing?.reasoning ?? null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerKey(provider: CatalogProvider, key: string): string {
|
||||||
|
return `${provider}\0${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AiModelCatalog {
|
||||||
|
private entries: AiModelCatalogEntry[] = [];
|
||||||
|
private byProvider = new Map<CatalogProvider, AiModelCatalogEntry[]>();
|
||||||
|
private byProviderAndKey = new Map<string, AiModelCatalogEntry>();
|
||||||
|
private byKey = new Map<string, AiModelCatalogEntry[]>();
|
||||||
|
private refreshTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the catalog into memory and schedules periodic background refreshes.
|
||||||
|
* Call once at server startup.
|
||||||
|
*/
|
||||||
|
async init(): Promise<void> {
|
||||||
|
await this.refresh();
|
||||||
|
this.scheduleNextRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exact lookup by catalog provider and model key. */
|
||||||
|
get(
|
||||||
|
provider: CatalogProvider,
|
||||||
|
key: string
|
||||||
|
): AiModelCatalogEntry | undefined {
|
||||||
|
return this.byProviderAndKey.get(providerKey(provider, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All models for a catalog provider. */
|
||||||
|
list(provider: CatalogProvider): AiModelCatalogEntry[] {
|
||||||
|
return this.byProvider.get(provider) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All catalog entries that share a model key, across providers. */
|
||||||
|
listByKey(key: string): AiModelCatalogEntry[] {
|
||||||
|
return this.byKey.get(key) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full in-memory catalog. */
|
||||||
|
getAll(): AiModelCatalogEntry[] {
|
||||||
|
return this.entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setEntries(entries: AiModelCatalogEntry[]): void {
|
||||||
|
const byProvider = new Map<CatalogProvider, AiModelCatalogEntry[]>();
|
||||||
|
const byProviderAndKey = new Map<string, AiModelCatalogEntry>();
|
||||||
|
const byKey = new Map<string, AiModelCatalogEntry[]>();
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const list = byProvider.get(entry.provider) ?? [];
|
||||||
|
list.push(entry);
|
||||||
|
byProvider.set(entry.provider, list);
|
||||||
|
|
||||||
|
const mapKey = providerKey(entry.provider, entry.model);
|
||||||
|
if (!byProviderAndKey.has(mapKey)) {
|
||||||
|
byProviderAndKey.set(mapKey, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyList = byKey.get(entry.model) ?? [];
|
||||||
|
keyList.push(entry);
|
||||||
|
byKey.set(entry.model, keyList);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entries = entries;
|
||||||
|
this.byProvider = byProvider;
|
||||||
|
this.byProviderAndKey = byProviderAndKey;
|
||||||
|
this.byKey = byKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchFromFile(
|
||||||
|
filePath: string
|
||||||
|
): Promise<AiModelCatalogEntry[] | null> {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
logger.warn(
|
||||||
|
`AI model catalog file not found at ${filePath}; cost calculation will fall back to unknown pricing`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const result = catalogFileSchema.safeParse(JSON.parse(raw));
|
||||||
|
if (!result.success) {
|
||||||
|
logger.warn(
|
||||||
|
`AI model catalog file at ${filePath} failed validation: ${result.error.message}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return result.data.data
|
||||||
|
.map(normalizeEntry)
|
||||||
|
.filter((e): e is AiModelCatalogEntry => e != null);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("Failed to read AI model catalog file", { error });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchFromUpstream(
|
||||||
|
upstreamUrl: string
|
||||||
|
): Promise<AiModelCatalogEntry[] | null> {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(upstreamUrl, { timeout: 15_000 });
|
||||||
|
const result = catalogFileSchema.safeParse(res.data);
|
||||||
|
if (!result.success) {
|
||||||
|
logger.warn(
|
||||||
|
`AI model catalog response from ${upstreamUrl} failed validation: ${result.error.message}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return result.data.data
|
||||||
|
.map(normalizeEntry)
|
||||||
|
.filter((e): e is AiModelCatalogEntry => e != null);
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to fetch AI model catalog from ${upstreamUrl}: ${error.message || error}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refresh(): Promise<void> {
|
||||||
|
const { file, merge_file, upstream_url } =
|
||||||
|
config.getRawConfig().ai.model_catalog;
|
||||||
|
|
||||||
|
const fetched = file
|
||||||
|
? await this.fetchFromFile(file)
|
||||||
|
: await this.fetchFromUpstream(upstream_url);
|
||||||
|
|
||||||
|
if (!fetched) {
|
||||||
|
logger.debug(
|
||||||
|
"AI model catalog refresh failed; keeping previously loaded catalog in memory"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let merged = fetched;
|
||||||
|
if (merge_file) {
|
||||||
|
const mergeEntries = await this.fetchFromFile(merge_file);
|
||||||
|
if (mergeEntries) {
|
||||||
|
// Entries from the base catalog take precedence; the merge
|
||||||
|
// file only adds models not already present.
|
||||||
|
merged = [...fetched, ...mergeEntries];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setEntries(merged);
|
||||||
|
logger.debug(
|
||||||
|
`AI model catalog refreshed: ${this.entries.length} models loaded`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleNextRefresh(): void {
|
||||||
|
const { refresh_interval_min_hours, refresh_interval_max_hours } =
|
||||||
|
config.getRawConfig().ai.model_catalog;
|
||||||
|
|
||||||
|
// Jittered rather than fixed so that many self-hosted instances don't
|
||||||
|
// all hit the upstream catalog endpoint at the same moment.
|
||||||
|
const minMs = refresh_interval_min_hours * 60 * 60 * 1000;
|
||||||
|
const maxMs = refresh_interval_max_hours * 60 * 60 * 1000;
|
||||||
|
const delayMs = minMs + Math.random() * Math.max(0, maxMs - minMs);
|
||||||
|
|
||||||
|
if (this.refreshTimer) {
|
||||||
|
clearTimeout(this.refreshTimer);
|
||||||
|
}
|
||||||
|
this.refreshTimer = setTimeout(async () => {
|
||||||
|
await this.refresh();
|
||||||
|
this.scheduleNextRefresh();
|
||||||
|
}, delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiModelCatalog = new AiModelCatalog();
|
||||||
|
|
||||||
|
export function listCatalogModelsForType(
|
||||||
|
type: AiProviderType,
|
||||||
|
query?: string
|
||||||
|
): { model: string }[] {
|
||||||
|
const catalogProvider = getCatalogProviderForType(type);
|
||||||
|
|
||||||
|
let models = catalogProvider
|
||||||
|
? aiModelCatalog.list(catalogProvider).map((entry) => ({
|
||||||
|
model: entry.model
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
models = models.filter((m) => m.model.toLowerCase().includes(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
models = models.filter((m) => {
|
||||||
|
if (seen.has(m.model)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(m.model);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
models.sort((a, b) => a.model.localeCompare(b.model));
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the AI model pricing catalog into memory and schedules periodic
|
||||||
|
* background refreshes. Call once at server startup.
|
||||||
|
*/
|
||||||
|
export async function initAiModelCatalog(): Promise<void> {
|
||||||
|
await aiModelCatalog.init();
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
const modelKeyRegexCache = new Map<string, RegExp>();
|
||||||
|
|
||||||
|
export function isModelKeyPattern(key: string): boolean {
|
||||||
|
return key.includes("*") || key.includes("?");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModelKeyRegex(pattern: string): RegExp {
|
||||||
|
let regex = modelKeyRegexCache.get(pattern);
|
||||||
|
if (!regex) {
|
||||||
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
||||||
|
regex = new RegExp(
|
||||||
|
`^${escaped.replace(/\*/g, ".*").replace(/\?/g, ".")}$`
|
||||||
|
);
|
||||||
|
modelKeyRegexCache.set(pattern, regex);
|
||||||
|
}
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modelKeyMatches(
|
||||||
|
pattern: string,
|
||||||
|
requestedModel: string
|
||||||
|
): boolean {
|
||||||
|
return getModelKeyRegex(pattern).test(requestedModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wildcardCharCount(key: string): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const char of key) {
|
||||||
|
if (char === "*" || char === "?") {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function literalLength(key: string): number {
|
||||||
|
return key.replace(/[*?]/g, "").length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort comparator: more specific patterns sort before less specific ones
|
||||||
|
* (negative when `a` is more specific than `b`).
|
||||||
|
*
|
||||||
|
* 1. Exact keys beat patterns
|
||||||
|
* 2. Fewer wildcard characters win
|
||||||
|
* 3. Longer literal length wins
|
||||||
|
*/
|
||||||
|
export function compareModelKeySpecificity(a: string, b: string): number {
|
||||||
|
const aIsPattern = isModelKeyPattern(a);
|
||||||
|
const bIsPattern = isModelKeyPattern(b);
|
||||||
|
|
||||||
|
if (aIsPattern !== bIsPattern) {
|
||||||
|
return aIsPattern ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wildcardDiff = wildcardCharCount(a) - wildcardCharCount(b);
|
||||||
|
if (wildcardDiff !== 0) {
|
||||||
|
return wildcardDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return literalLength(b) - literalLength(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider-layer policy: empty allowlist denies all. Blocklist only applies
|
||||||
|
* after an allow match.
|
||||||
|
*/
|
||||||
|
export function isAllowedByLists(
|
||||||
|
requested: string,
|
||||||
|
allows: string[],
|
||||||
|
blocks: string[]
|
||||||
|
): boolean {
|
||||||
|
if (allows.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!allows.some((pattern) => modelKeyMatches(pattern, requested))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (blocks.some((pattern) => modelKeyMatches(pattern, requested))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Among allow patterns that match `requested`, return the most specific one,
|
||||||
|
* or null if none match.
|
||||||
|
*/
|
||||||
|
export function mostSpecificMatchingAllow(
|
||||||
|
requested: string,
|
||||||
|
allows: string[]
|
||||||
|
): string | null {
|
||||||
|
const matching = allows.filter((pattern) =>
|
||||||
|
modelKeyMatches(pattern, requested)
|
||||||
|
);
|
||||||
|
if (matching.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
matching.sort(compareModelKeySpecificity);
|
||||||
|
return matching[0];
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import type { AiProviderType } from "@server/lib/aiProviderDefaults";
|
||||||
|
import type { AiUsage } from "@server/lib/aiUsageExtraction";
|
||||||
|
import {
|
||||||
|
aiModelCatalog,
|
||||||
|
getCatalogProviderForType,
|
||||||
|
type AiModelCatalogEntry,
|
||||||
|
type CatalogProvider
|
||||||
|
} from "@server/lib/aiModelCatalog";
|
||||||
|
|
||||||
|
export type AiModelPricing = {
|
||||||
|
inputCostPerToken: number | null;
|
||||||
|
outputCostPerToken: number | null;
|
||||||
|
cacheReadInputTokenCost: number | null;
|
||||||
|
outputCostPerReasoningToken: number | null;
|
||||||
|
// True when the match came from a different catalog provider than the
|
||||||
|
// one mapped to this provider's type (e.g. an openRouter/custom model
|
||||||
|
// id that only matched a global search across every provider). Costs
|
||||||
|
// found this way are a best-effort approximation, not a guarantee the
|
||||||
|
// upstream provider bills at the same rate.
|
||||||
|
approximate: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function stripVendorPrefix(modelId: string): string | null {
|
||||||
|
const idx = modelId.indexOf("/");
|
||||||
|
if (idx === -1 || idx === modelId.length - 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return modelId.slice(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPricing(
|
||||||
|
entry: AiModelCatalogEntry,
|
||||||
|
approximate: boolean
|
||||||
|
): AiModelPricing {
|
||||||
|
return {
|
||||||
|
inputCostPerToken: entry.pricing.in,
|
||||||
|
outputCostPerToken: entry.pricing.out,
|
||||||
|
cacheReadInputTokenCost: entry.pricing.cache,
|
||||||
|
outputCostPerReasoningToken: entry.pricing.reasoning,
|
||||||
|
approximate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEntry(
|
||||||
|
modelId: string,
|
||||||
|
provider: CatalogProvider | null
|
||||||
|
): AiModelCatalogEntry | null {
|
||||||
|
const candidates = [modelId, stripVendorPrefix(modelId)].filter(
|
||||||
|
(v): v is string => v != null
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const key of candidates) {
|
||||||
|
if (provider) {
|
||||||
|
const match = aiModelCatalog.get(provider, key);
|
||||||
|
if (match) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = aiModelCatalog.listByKey(key)[0];
|
||||||
|
if (match) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up per-token pricing for a model, scoped first to the catalog
|
||||||
|
* provider that corresponds to our provider type, then falling back to a
|
||||||
|
* global search across every provider (marked `approximate`) for provider
|
||||||
|
* types that proxy arbitrary underlying models.
|
||||||
|
*/
|
||||||
|
export function getModelPricing(
|
||||||
|
providerType: AiProviderType,
|
||||||
|
modelId: string | undefined
|
||||||
|
): AiModelPricing | null {
|
||||||
|
if (!modelId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalogProvider = getCatalogProviderForType(providerType);
|
||||||
|
|
||||||
|
if (catalogProvider) {
|
||||||
|
const scoped = findEntry(modelId, catalogProvider);
|
||||||
|
if (scoped) {
|
||||||
|
return toPricing(scoped, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = findEntry(modelId, null);
|
||||||
|
if (fallback) {
|
||||||
|
return toPricing(fallback, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AiCostBreakdown = {
|
||||||
|
promptCost: number;
|
||||||
|
cacheReadCost: number;
|
||||||
|
cacheWriteCost: number;
|
||||||
|
completionCost: number;
|
||||||
|
reasoningCost: number;
|
||||||
|
totalCost: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a $ cost breakdown for a usage record given a model's pricing.
|
||||||
|
* Cache writes and reasoning tokens fall back to the normal input/output
|
||||||
|
* rate respectively when the catalog has no dedicated rate for them (the
|
||||||
|
* catalog has no cache-write field at all, and only some models report a
|
||||||
|
* distinct reasoning rate).
|
||||||
|
*/
|
||||||
|
export function calculateAiCost(
|
||||||
|
pricing: AiModelPricing | null,
|
||||||
|
usage: AiUsage
|
||||||
|
): AiCostBreakdown | null {
|
||||||
|
if (!pricing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputRate = pricing.inputCostPerToken ?? 0;
|
||||||
|
const outputRate = pricing.outputCostPerToken ?? 0;
|
||||||
|
const cacheReadRate = pricing.cacheReadInputTokenCost ?? inputRate;
|
||||||
|
const reasoningRate = pricing.outputCostPerReasoningToken ?? outputRate;
|
||||||
|
|
||||||
|
const promptCost = usage.promptTokens * inputRate;
|
||||||
|
const cacheReadCost = usage.cacheReadTokens * cacheReadRate;
|
||||||
|
const cacheWriteCost = usage.cacheWriteTokens * inputRate;
|
||||||
|
const completionCost = usage.completionTokens * outputRate;
|
||||||
|
const reasoningCost = usage.reasoningTokens * reasoningRate;
|
||||||
|
|
||||||
|
return {
|
||||||
|
promptCost,
|
||||||
|
cacheReadCost,
|
||||||
|
cacheWriteCost,
|
||||||
|
completionCost,
|
||||||
|
reasoningCost,
|
||||||
|
totalCost:
|
||||||
|
promptCost +
|
||||||
|
cacheReadCost +
|
||||||
|
cacheWriteCost +
|
||||||
|
completionCost +
|
||||||
|
reasoningCost
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { decrypt, encrypt } from "@server/lib/crypto";
|
||||||
|
import {
|
||||||
|
parseCapabilities,
|
||||||
|
type AiCapability
|
||||||
|
} from "@server/lib/aiCapabilities";
|
||||||
|
import { stripVirtualApiKeyAuthHeaders } from "@app/lib/virtualApiKeyFormat";
|
||||||
|
import {
|
||||||
|
AI_PROVIDER_AUTH_TYPES,
|
||||||
|
AI_PROVIDER_DEFAULTS,
|
||||||
|
authTypeRequiresApiKey,
|
||||||
|
defaultsForProviderType,
|
||||||
|
providerRequiresUpstreamUrl,
|
||||||
|
type AiBudgetUnit,
|
||||||
|
type AiProviderAuthType,
|
||||||
|
type AiProviderRoutingMode,
|
||||||
|
type AiProviderType
|
||||||
|
} from "@app/lib/aiProviderDefaults";
|
||||||
|
|
||||||
|
export {
|
||||||
|
AI_PROVIDER_AUTH_TYPES,
|
||||||
|
AI_PROVIDER_DEFAULTS,
|
||||||
|
authTypeRequiresApiKey,
|
||||||
|
defaultsForProviderType,
|
||||||
|
providerRequiresUpstreamUrl,
|
||||||
|
type AiBudgetUnit,
|
||||||
|
type AiProviderAuthType,
|
||||||
|
type AiProviderRoutingMode,
|
||||||
|
type AiProviderType
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONFLICTING_AUTH_HEADERS = [
|
||||||
|
"authorization",
|
||||||
|
"x-api-key",
|
||||||
|
"x-goog-api-key",
|
||||||
|
"cf-aig-authorization"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function resolveAiProviderCreateFields(input: {
|
||||||
|
type: AiProviderType;
|
||||||
|
upstreamUrl?: string | null;
|
||||||
|
authType?: AiProviderAuthType | null;
|
||||||
|
routingMode?: AiProviderRoutingMode | null;
|
||||||
|
}): {
|
||||||
|
upstreamUrl: string | null;
|
||||||
|
authType: AiProviderAuthType;
|
||||||
|
routingMode: AiProviderRoutingMode;
|
||||||
|
} {
|
||||||
|
const routingMode =
|
||||||
|
input.type === "custom" ? (input.routingMode ?? "url") : "url";
|
||||||
|
|
||||||
|
if (routingMode === "target") {
|
||||||
|
return {
|
||||||
|
upstreamUrl: null,
|
||||||
|
authType: input.authType ?? "bearer",
|
||||||
|
routingMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.type === "custom") {
|
||||||
|
return {
|
||||||
|
upstreamUrl: input.upstreamUrl ?? null,
|
||||||
|
authType: input.authType ?? "bearer",
|
||||||
|
routingMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = AI_PROVIDER_DEFAULTS[input.type];
|
||||||
|
return {
|
||||||
|
upstreamUrl: input.upstreamUrl ?? defaults.upstreamUrl,
|
||||||
|
authType: input.authType ?? defaults.authType,
|
||||||
|
routingMode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AiProviderHeader = { name: string; value: string };
|
||||||
|
|
||||||
|
export function serializeAiProviderHeaders(
|
||||||
|
headers: AiProviderHeader[] | null | undefined,
|
||||||
|
secret: string
|
||||||
|
): string | null {
|
||||||
|
if (!headers || headers.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return encrypt(JSON.stringify(headers), secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAiProviderHeaders(
|
||||||
|
raw: string | null | undefined,
|
||||||
|
secret: string
|
||||||
|
): AiProviderHeader[] {
|
||||||
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const decrypted = decrypt(raw, secret);
|
||||||
|
const parsed = JSON.parse(decrypted);
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return parsed.filter(
|
||||||
|
(h): h is AiProviderHeader =>
|
||||||
|
h != null &&
|
||||||
|
typeof h === "object" &&
|
||||||
|
typeof h.name === "string" &&
|
||||||
|
typeof h.value === "string"
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAiProviderCustomHeaders(
|
||||||
|
headers: Record<string, string>,
|
||||||
|
raw: string | null | undefined,
|
||||||
|
secret: string
|
||||||
|
): void {
|
||||||
|
for (const { name, value } of parseAiProviderHeaders(raw, secret)) {
|
||||||
|
headers[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply provider auth to upstream headers.
|
||||||
|
* - Always strips Pangolin virtual API key credentials from client auth headers.
|
||||||
|
* - Injected modes: strip conflicting client auth headers, then set the provider key.
|
||||||
|
* - none: strip conflicting client auth headers, send no auth.
|
||||||
|
* - passthrough: leave remaining client auth headers as-is (after VAK strip).
|
||||||
|
*/
|
||||||
|
export function applyAiProviderAuthHeaders(
|
||||||
|
headers: Record<string, string>,
|
||||||
|
authType: AiProviderAuthType,
|
||||||
|
apiKey: string | null
|
||||||
|
): void {
|
||||||
|
stripVirtualApiKeyAuthHeaders(headers);
|
||||||
|
|
||||||
|
if (authType === "passthrough") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of CONFLICTING_AUTH_HEADERS) {
|
||||||
|
for (const key of Object.keys(headers)) {
|
||||||
|
if (key.toLowerCase() === name) {
|
||||||
|
delete headers[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authType === "none") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error(`API key required for authType ${authType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (authType) {
|
||||||
|
case "bearer":
|
||||||
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
||||||
|
break;
|
||||||
|
case "x-api-key":
|
||||||
|
headers["x-api-key"] = apiKey;
|
||||||
|
break;
|
||||||
|
case "x-goog-api-key":
|
||||||
|
headers["x-goog-api-key"] = apiKey;
|
||||||
|
break;
|
||||||
|
case "hec":
|
||||||
|
headers["Authorization"] = `Splunk ${apiKey}`;
|
||||||
|
break;
|
||||||
|
case "cf-aig-authorization":
|
||||||
|
headers["cf-aig-authorization"] = `Bearer ${apiKey}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveCapabilitiesForCreate(input: {
|
||||||
|
type: AiProviderType;
|
||||||
|
capabilities?: AiCapability[] | null;
|
||||||
|
}): AiCapability[] {
|
||||||
|
if (input.capabilities != null) {
|
||||||
|
return parseCapabilities(input.capabilities);
|
||||||
|
}
|
||||||
|
if (input.type === "custom") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [...AI_PROVIDER_DEFAULTS[input.type].capabilities];
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
aiModelCatalog,
|
||||||
|
getCatalogProviderForType,
|
||||||
|
type CatalogProvider
|
||||||
|
} from "@server/lib/aiModelCatalog";
|
||||||
|
import type { AiProviderType } from "@server/lib/aiProviderDefaults";
|
||||||
|
|
||||||
|
function stripVendorPrefix(modelId: string): string | null {
|
||||||
|
const idx = modelId.indexOf("/");
|
||||||
|
if (idx === -1 || idx === modelId.length - 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return modelId.slice(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelKeysToTry(modelId: string): string[] {
|
||||||
|
const keys = [modelId];
|
||||||
|
const stripped = stripVendorPrefix(modelId);
|
||||||
|
if (stripped) {
|
||||||
|
keys.push(stripped);
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function catalogOwnsModel(
|
||||||
|
catalogProvider: CatalogProvider,
|
||||||
|
modelId: string
|
||||||
|
): boolean {
|
||||||
|
for (const key of modelKeysToTry(modelId)) {
|
||||||
|
if (aiModelCatalog.get(catalogProvider, key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelKnownInAnyCatalog(modelId: string): boolean {
|
||||||
|
for (const key of modelKeysToTry(modelId)) {
|
||||||
|
if (aiModelCatalog.listByKey(key).length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How strongly a provider "owns" a requested model id via the known catalog.
|
||||||
|
*
|
||||||
|
* 2 - Typed provider whose catalog contains the model
|
||||||
|
* 1 - Aggregator/custom that can proxy a catalog-known model
|
||||||
|
* 0 - No ownership signal (typed miss, or unknown model on aggregator/custom)
|
||||||
|
*/
|
||||||
|
export function catalogOwnershipScore(
|
||||||
|
type: AiProviderType,
|
||||||
|
modelId: string
|
||||||
|
): number {
|
||||||
|
const catalogProvider = getCatalogProviderForType(type);
|
||||||
|
if (catalogProvider != null) {
|
||||||
|
return catalogOwnsModel(catalogProvider, modelId) ? 2 : 0;
|
||||||
|
}
|
||||||
|
return modelKnownInAnyCatalog(modelId) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefer native vendor providers over aggregators over custom when catalog
|
||||||
|
* ownership is tied.
|
||||||
|
*
|
||||||
|
* 2 - Native typed provider (openai, anthropic, gemini, ...)
|
||||||
|
* 1 - Aggregator gateway (openRouter, vercelAiGateway)
|
||||||
|
* 0 - Custom
|
||||||
|
*/
|
||||||
|
export function providerClassRank(type: AiProviderType): number {
|
||||||
|
if (type === "custom") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (type === "openRouter" || type === "vercelAiGateway") {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keepBestScored<T>(
|
||||||
|
items: T[],
|
||||||
|
scoreFn: (item: T) => number
|
||||||
|
): T[] {
|
||||||
|
if (items.length <= 1) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
let best = Number.NEGATIVE_INFINITY;
|
||||||
|
for (const item of items) {
|
||||||
|
const score = scoreFn(item);
|
||||||
|
if (score > best) {
|
||||||
|
best = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items.filter((item) => scoreFn(item) === best);
|
||||||
|
}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
import { encode } from "gpt-tokenizer";
|
||||||
|
import type { AiCapability } from "@server/lib/aiCapabilities";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export type AiUsage = {
|
||||||
|
// Input tokens billed at the normal input rate (i.e. NOT already
|
||||||
|
// covered by cacheReadTokens/cacheWriteTokens below).
|
||||||
|
promptTokens: number;
|
||||||
|
cacheReadTokens: number;
|
||||||
|
cacheWriteTokens: number;
|
||||||
|
// Output tokens billed at the normal output rate (i.e. NOT already
|
||||||
|
// covered by reasoningTokens below).
|
||||||
|
completionTokens: number;
|
||||||
|
reasoningTokens: number;
|
||||||
|
// True when these numbers are our own best-guess estimate (the upstream
|
||||||
|
// response didn't report usage), rather than provider-reported figures.
|
||||||
|
estimated: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function emptyUsage(): AiUsage {
|
||||||
|
return {
|
||||||
|
promptTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cacheWriteTokens: 0,
|
||||||
|
completionTokens: 0,
|
||||||
|
reasoningTokens: 0,
|
||||||
|
estimated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans raw (possibly binary-framed, e.g. Bedrock's vnd.amazon.eventstream)
|
||||||
|
* text for `"fieldName":123` occurrences and returns the last value seen for
|
||||||
|
* each field. Used as a best-effort fallback for response shapes we can't
|
||||||
|
* fully parse as JSON/SSE (streaming Bedrock, raw predict passthroughs).
|
||||||
|
*/
|
||||||
|
function scanNumericFields(
|
||||||
|
text: string,
|
||||||
|
fields: string[]
|
||||||
|
): Record<string, number> {
|
||||||
|
const out: Record<string, number> = {};
|
||||||
|
for (const field of fields) {
|
||||||
|
const re = new RegExp(`"${field}"\\s*:\\s*(\\d+)`, "g");
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = re.exec(text)) !== null) {
|
||||||
|
out[field] = Number(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exported for reuse by server/lib/aiMessageNormalization.ts, which needs
|
||||||
|
// the same SSE-frame/JSON-parsing groundwork to extract message content
|
||||||
|
// instead of usage numbers.
|
||||||
|
export function sseDataFrames(text: string): string[] {
|
||||||
|
const frames: string[] = [];
|
||||||
|
for (const rawFrame of text.split(/\r?\n\r?\n/)) {
|
||||||
|
for (const line of rawFrame.split(/\r?\n/)) {
|
||||||
|
if (!line.startsWith("data:")) continue;
|
||||||
|
const data = line.slice("data:".length).trim();
|
||||||
|
if (data && data !== "[DONE]") {
|
||||||
|
frames.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tryParseJson(text: string): any | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractOpenAiChat(text: string, isStream: boolean): AiUsage | null {
|
||||||
|
let usage: any = null;
|
||||||
|
|
||||||
|
if (isStream) {
|
||||||
|
for (const frame of sseDataFrames(text)) {
|
||||||
|
const parsed = tryParseJson(frame);
|
||||||
|
if (parsed?.usage) {
|
||||||
|
usage = parsed.usage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
usage = tryParseJson(text)?.usage ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0;
|
||||||
|
const reasoningTokens =
|
||||||
|
usage.completion_tokens_details?.reasoning_tokens ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
promptTokens: Math.max(0, (usage.prompt_tokens ?? 0) - cacheReadTokens),
|
||||||
|
cacheReadTokens,
|
||||||
|
cacheWriteTokens: 0,
|
||||||
|
completionTokens: Math.max(
|
||||||
|
0,
|
||||||
|
(usage.completion_tokens ?? 0) - reasoningTokens
|
||||||
|
),
|
||||||
|
reasoningTokens,
|
||||||
|
estimated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractOpenAiResponses(
|
||||||
|
text: string,
|
||||||
|
isStream: boolean
|
||||||
|
): AiUsage | null {
|
||||||
|
let usage: any = null;
|
||||||
|
|
||||||
|
if (isStream) {
|
||||||
|
for (const frame of sseDataFrames(text)) {
|
||||||
|
const parsed = tryParseJson(frame);
|
||||||
|
if (
|
||||||
|
parsed?.type === "response.completed" &&
|
||||||
|
parsed?.response?.usage
|
||||||
|
) {
|
||||||
|
usage = parsed.response.usage;
|
||||||
|
} else if (parsed?.usage) {
|
||||||
|
usage = parsed.usage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const parsed = tryParseJson(text);
|
||||||
|
usage = parsed?.usage ?? parsed?.response?.usage ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheReadTokens = usage.input_tokens_details?.cached_tokens ?? 0;
|
||||||
|
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
promptTokens: Math.max(0, (usage.input_tokens ?? 0) - cacheReadTokens),
|
||||||
|
cacheReadTokens,
|
||||||
|
cacheWriteTokens: 0,
|
||||||
|
completionTokens: Math.max(
|
||||||
|
0,
|
||||||
|
(usage.output_tokens ?? 0) - reasoningTokens
|
||||||
|
),
|
||||||
|
reasoningTokens,
|
||||||
|
estimated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAnthropicMessages(
|
||||||
|
text: string,
|
||||||
|
isStream: boolean
|
||||||
|
): AiUsage | null {
|
||||||
|
let inputTokens = 0;
|
||||||
|
let cacheReadTokens = 0;
|
||||||
|
let cacheWriteTokens = 0;
|
||||||
|
let outputTokens = 0;
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
const applyUsage = (usage: any) => {
|
||||||
|
if (!usage) return;
|
||||||
|
found = true;
|
||||||
|
if (typeof usage.input_tokens === "number") {
|
||||||
|
inputTokens = usage.input_tokens;
|
||||||
|
}
|
||||||
|
if (typeof usage.cache_read_input_tokens === "number") {
|
||||||
|
cacheReadTokens = usage.cache_read_input_tokens;
|
||||||
|
}
|
||||||
|
if (typeof usage.cache_creation_input_tokens === "number") {
|
||||||
|
cacheWriteTokens = usage.cache_creation_input_tokens;
|
||||||
|
}
|
||||||
|
if (typeof usage.output_tokens === "number") {
|
||||||
|
outputTokens = usage.output_tokens;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isStream) {
|
||||||
|
for (const frame of sseDataFrames(text)) {
|
||||||
|
const parsed = tryParseJson(frame);
|
||||||
|
if (!parsed) continue;
|
||||||
|
applyUsage(parsed.message?.usage);
|
||||||
|
applyUsage(parsed.usage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
applyUsage(tryParseJson(text)?.usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
promptTokens: inputTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
cacheWriteTokens,
|
||||||
|
completionTokens: outputTokens,
|
||||||
|
// Anthropic bills extended-thinking output at the normal output
|
||||||
|
// rate, so there's no separate reasoning bucket to report.
|
||||||
|
reasoningTokens: 0,
|
||||||
|
estimated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractGoogleGenerateContent(
|
||||||
|
text: string,
|
||||||
|
_isStream: boolean
|
||||||
|
): AiUsage | null {
|
||||||
|
// Both the plain-JSON-array stream format and the SSE (?alt=sse) format
|
||||||
|
// repeat a cumulative `usageMetadata` object per chunk; the regex scan
|
||||||
|
// below naturally picks up the last (most complete) one either way.
|
||||||
|
const fields = scanNumericFields(text, [
|
||||||
|
"promptTokenCount",
|
||||||
|
"candidatesTokenCount",
|
||||||
|
"cachedContentTokenCount",
|
||||||
|
"thoughtsTokenCount"
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (fields.promptTokenCount === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheReadTokens = fields.cachedContentTokenCount ?? 0;
|
||||||
|
const reasoningTokens = fields.thoughtsTokenCount ?? 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
promptTokens: Math.max(0, fields.promptTokenCount - cacheReadTokens),
|
||||||
|
cacheReadTokens,
|
||||||
|
cacheWriteTokens: 0,
|
||||||
|
completionTokens: fields.candidatesTokenCount ?? 0,
|
||||||
|
reasoningTokens,
|
||||||
|
estimated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractBedrockConverse(
|
||||||
|
text: string,
|
||||||
|
_isStream: boolean
|
||||||
|
): AiUsage | null {
|
||||||
|
// Non-streaming responses are plain JSON; converse-stream frames the
|
||||||
|
// final `metadata` event's usage object inside binary event-stream
|
||||||
|
// framing, but the JSON text survives intact inside that binary
|
||||||
|
// envelope, so the same field scan works for both.
|
||||||
|
const parsed = tryParseJson(text);
|
||||||
|
const usage = parsed?.usage;
|
||||||
|
if (usage) {
|
||||||
|
const cacheReadTokens = usage.cacheReadInputTokens ?? 0;
|
||||||
|
return {
|
||||||
|
promptTokens: Math.max(
|
||||||
|
0,
|
||||||
|
(usage.inputTokens ?? 0) - cacheReadTokens
|
||||||
|
),
|
||||||
|
cacheReadTokens,
|
||||||
|
cacheWriteTokens: usage.cacheWriteInputTokens ?? 0,
|
||||||
|
completionTokens: usage.outputTokens ?? 0,
|
||||||
|
reasoningTokens: 0,
|
||||||
|
estimated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = scanNumericFields(text, [
|
||||||
|
"inputTokens",
|
||||||
|
"outputTokens",
|
||||||
|
"cacheReadInputTokens",
|
||||||
|
"cacheWriteInputTokens"
|
||||||
|
]);
|
||||||
|
if (fields.inputTokens === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const cacheReadTokens = fields.cacheReadInputTokens ?? 0;
|
||||||
|
return {
|
||||||
|
promptTokens: Math.max(0, fields.inputTokens - cacheReadTokens),
|
||||||
|
cacheReadTokens,
|
||||||
|
cacheWriteTokens: fields.cacheWriteInputTokens ?? 0,
|
||||||
|
completionTokens: fields.outputTokens ?? 0,
|
||||||
|
reasoningTokens: 0,
|
||||||
|
estimated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractBedrockModelInvoke(
|
||||||
|
text: string,
|
||||||
|
_isStream: boolean,
|
||||||
|
headers: Headers
|
||||||
|
): AiUsage | null {
|
||||||
|
// Non-streaming invoke reports counts via response headers regardless
|
||||||
|
// of the underlying model's payload format.
|
||||||
|
const headerInput = headers.get("x-amzn-bedrock-input-token-count");
|
||||||
|
const headerOutput = headers.get("x-amzn-bedrock-output-token-count");
|
||||||
|
if (headerInput !== null || headerOutput !== null) {
|
||||||
|
return {
|
||||||
|
promptTokens: Number(headerInput ?? 0),
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cacheWriteTokens: 0,
|
||||||
|
completionTokens: Number(headerOutput ?? 0),
|
||||||
|
reasoningTokens: 0,
|
||||||
|
estimated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// invoke-with-response-stream has no equivalent headers; the model's
|
||||||
|
// own usage shape (frequently Anthropic-style on Bedrock) is embedded
|
||||||
|
// inside binary event-stream framing, so fall back to a couple of
|
||||||
|
// known field-name shapes via regex.
|
||||||
|
const anthropicStyle = extractAnthropicMessages(text, true);
|
||||||
|
if (anthropicStyle) {
|
||||||
|
return anthropicStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = scanNumericFields(text, [
|
||||||
|
"inputTokenCount",
|
||||||
|
"outputTokenCount"
|
||||||
|
]);
|
||||||
|
if (fields.inputTokenCount === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
promptTokens: fields.inputTokenCount,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cacheWriteTokens: 0,
|
||||||
|
completionTokens: fields.outputTokenCount ?? 0,
|
||||||
|
reasoningTokens: 0,
|
||||||
|
estimated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXTRACTORS: Record<
|
||||||
|
AiCapability,
|
||||||
|
(text: string, isStream: boolean, headers: Headers) => AiUsage | null
|
||||||
|
> = {
|
||||||
|
openai_chat: extractOpenAiChat,
|
||||||
|
openai_responses: extractOpenAiResponses,
|
||||||
|
anthropic_messages: extractAnthropicMessages,
|
||||||
|
gemini_generate_content: extractGoogleGenerateContent,
|
||||||
|
google_generate_content: extractGoogleGenerateContent,
|
||||||
|
// rawPredict is a passthrough to whatever the underlying publisher
|
||||||
|
// model speaks (often Anthropic-shaped on Vertex); try that, then give
|
||||||
|
// up to the token-count estimate.
|
||||||
|
google_raw_predict: (text, isStream) =>
|
||||||
|
extractAnthropicMessages(text, isStream),
|
||||||
|
bedrock_model_invoke: extractBedrockModelInvoke,
|
||||||
|
bedrock_converse: extractBedrockConverse
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to pull provider-reported token usage out of an upstream AI
|
||||||
|
* gateway response. Returns null if the response didn't contain (or we
|
||||||
|
* couldn't find) usage data, in which case callers should fall back to
|
||||||
|
* `estimateUsage`.
|
||||||
|
*/
|
||||||
|
export function extractUsage(
|
||||||
|
capability: AiCapability,
|
||||||
|
responseText: string,
|
||||||
|
isStream: boolean,
|
||||||
|
headers: Headers
|
||||||
|
): AiUsage | null {
|
||||||
|
try {
|
||||||
|
return EXTRACTORS[capability](responseText, isStream, headers);
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug("Failed to extract AI usage from response", {
|
||||||
|
capability,
|
||||||
|
error
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-guess token estimate for when the provider doesn't report usage.
|
||||||
|
* Uses OpenAI's BPE tokenizer as a stand-in for whatever tokenizer the
|
||||||
|
* actual model uses - close enough for an approximate cost figure, not
|
||||||
|
* exact for non-OpenAI models.
|
||||||
|
*/
|
||||||
|
export function estimateUsage(
|
||||||
|
promptText: string,
|
||||||
|
completionText: string
|
||||||
|
): AiUsage {
|
||||||
|
const usage = emptyUsage();
|
||||||
|
usage.estimated = true;
|
||||||
|
try {
|
||||||
|
usage.promptTokens = promptText ? encode(promptText).length : 0;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug("Failed to estimate prompt tokens", { error });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
usage.completionTokens = completionText
|
||||||
|
? encode(completionText).length
|
||||||
|
: 0;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug("Failed to estimate completion tokens", { error });
|
||||||
|
}
|
||||||
|
return usage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI's Chat Completions API only includes a `usage` field in a
|
||||||
|
* streaming response when the request opts in via `stream_options:
|
||||||
|
* {include_usage: true}` - unlike the Responses API, Anthropic, Gemini and
|
||||||
|
* Bedrock, which report usage in a streaming response by default. Returns
|
||||||
|
* whether we need to inject that option ourselves to be able to track cost.
|
||||||
|
*/
|
||||||
|
export function needsStreamUsageInjection(
|
||||||
|
capability: AiCapability,
|
||||||
|
body: any
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
capability === "openai_chat" &&
|
||||||
|
body?.stream === true &&
|
||||||
|
body?.stream_options?.include_usage !== true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a shallow-cloned body with `stream_options.include_usage`
|
||||||
|
* injected, for capabilities/requests where `needsStreamUsageInjection`
|
||||||
|
* is true. Leaves the original body untouched.
|
||||||
|
*/
|
||||||
|
export function withStreamUsageOption(body: any): any {
|
||||||
|
return {
|
||||||
|
...body,
|
||||||
|
stream_options: { ...body.stream_options, include_usage: true }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When we injected stream_options.include_usage ourselves (the caller
|
||||||
|
* didn't ask for it), OpenAI appends an extra terminal SSE frame with an
|
||||||
|
* empty `choices: []` array carrying only the usage data. Callers that
|
||||||
|
* don't expect that shape (most minimal SSE parsers assume a non-empty
|
||||||
|
* choices array) shouldn't see it, so it's stripped back out of the bytes
|
||||||
|
* forwarded to the client.
|
||||||
|
*/
|
||||||
|
export function stripInjectedUsageFrame(sseText: string): string {
|
||||||
|
const parts = sseText.split(/(\r?\n\r?\n)/);
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < parts.length; i += 2) {
|
||||||
|
const frame = parts[i];
|
||||||
|
const separator = parts[i + 1] ?? "";
|
||||||
|
const dataLine = frame
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.find((line) => line.startsWith("data:"));
|
||||||
|
if (dataLine) {
|
||||||
|
const data = dataLine.slice("data:".length).trim();
|
||||||
|
const parsed = data !== "[DONE]" ? tryParseJson(data) : null;
|
||||||
|
if (
|
||||||
|
parsed &&
|
||||||
|
Array.isArray(parsed.choices) &&
|
||||||
|
parsed.choices.length === 0 &&
|
||||||
|
parsed.usage
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += frame + separator;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort extraction of the model the upstream provider actually
|
||||||
|
* served, which some gateways/routers echo back and which may differ from
|
||||||
|
* the model the caller requested (e.g. an alias resolving to a dated
|
||||||
|
* snapshot). Falls back to the caller's requested model when absent.
|
||||||
|
*/
|
||||||
|
export function extractResponseModel(responseText: string): string | null {
|
||||||
|
const match = responseText.match(/"model"\s*:\s*"([^"]+)"/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUsageEmpty(usage: AiUsage): boolean {
|
||||||
|
return (
|
||||||
|
usage.promptTokens === 0 &&
|
||||||
|
usage.cacheReadTokens === 0 &&
|
||||||
|
usage.cacheWriteTokens === 0 &&
|
||||||
|
usage.completionTokens === 0 &&
|
||||||
|
usage.reasoningTokens === 0
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -202,6 +202,10 @@ async function handleResource(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!target.resourceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [resource] = await trx
|
const [resource] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
@@ -227,9 +231,7 @@ async function handleResource(
|
|||||||
|
|
||||||
let health = "healthy";
|
let health = "healthy";
|
||||||
const allUnknown = monitoredTargets.length === 0;
|
const allUnknown = monitoredTargets.length === 0;
|
||||||
const allHealthy = monitoredTargets.every(
|
const allHealthy = monitoredTargets.every((t) => t.hcHealth === "healthy");
|
||||||
(t) => t.hcHealth === "healthy"
|
|
||||||
);
|
|
||||||
const allUnhealthy = monitoredTargets.every(
|
const allUnhealthy = monitoredTargets.every(
|
||||||
(t) => t.hcHealth === "unhealthy"
|
(t) => t.hcHealth === "unhealthy"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export enum TierFeature {
|
|||||||
ActionLogs = "actionLogs", // set the retention period to none on downgrade
|
ActionLogs = "actionLogs", // set the retention period to none on downgrade
|
||||||
ConnectionLogs = "connectionLogs",
|
ConnectionLogs = "connectionLogs",
|
||||||
RotateCredentials = "rotateCredentials",
|
RotateCredentials = "rotateCredentials",
|
||||||
MaintencePage = "maintencePage", // handle downgrade
|
MaintenancePage = "maintenancePage", // handle downgrade
|
||||||
DevicePosture = "devicePosture",
|
DevicePosture = "devicePosture",
|
||||||
TwoFactorEnforcement = "twoFactorEnforcement", // handle downgrade by setting to optional
|
TwoFactorEnforcement = "twoFactorEnforcement", // handle downgrade by setting to optional
|
||||||
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
|
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
|
||||||
@@ -25,8 +25,7 @@ export enum TierFeature {
|
|||||||
WildcardSubdomain = "wildcardSubdomain",
|
WildcardSubdomain = "wildcardSubdomain",
|
||||||
NewtAutoUpdate = "newtAutoUpdate",
|
NewtAutoUpdate = "newtAutoUpdate",
|
||||||
ResourcePolicies = "resourcePolicies",
|
ResourcePolicies = "resourcePolicies",
|
||||||
AdvancedPublicResources = "advancedPublicResources",
|
RoleBasedSSHControls = "roleBasedSSHControls"
|
||||||
AdvancedPrivateResources = "advancedPrivateResources"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
@@ -39,7 +38,7 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
[TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"],
|
[TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.ConnectionLogs]: ["tier2", "tier3", "enterprise"],
|
[TierFeature.ConnectionLogs]: ["tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.MaintencePage]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.MaintenancePage]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"],
|
[TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.TwoFactorEnforcement]: [
|
[TierFeature.TwoFactorEnforcement]: [
|
||||||
"tier1",
|
"tier1",
|
||||||
@@ -69,6 +68,5 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"],
|
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"],
|
||||||
[TierFeature.AdvancedPublicResources]: ["tier3", "enterprise"],
|
[TierFeature.RoleBasedSSHControls]: ["tier3", "enterprise"]
|
||||||
[TierFeature.AdvancedPrivateResources]: ["tier3", "enterprise"]
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { aiBudgets, Transaction } from "@server/db";
|
||||||
|
|
||||||
|
export type BlueprintAiBudgetInput = {
|
||||||
|
amount: number;
|
||||||
|
unit: "usd" | "tokens";
|
||||||
|
period:
|
||||||
|
| "monthly"
|
||||||
|
| "yearly"
|
||||||
|
| "lifetime"
|
||||||
|
| "daily"
|
||||||
|
| "hourly"
|
||||||
|
| "weekly";
|
||||||
|
enforcement: "hard" | "soft";
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SyncAiBudgetsInput = {
|
||||||
|
orgId: string;
|
||||||
|
trx: Transaction;
|
||||||
|
budgets: BlueprintAiBudgetInput[];
|
||||||
|
} & (
|
||||||
|
| { scope: "public"; resourceId: number }
|
||||||
|
| { scope: "site"; siteResourceId: number }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully declarative: makes the resource's/site resource's AI budgets match
|
||||||
|
* exactly what the blueprint declares (omitted unit/period budgets are removed).
|
||||||
|
*/
|
||||||
|
export async function syncAiBudgets(input: SyncAiBudgetsInput): Promise<void> {
|
||||||
|
const { orgId, trx, budgets } = input;
|
||||||
|
|
||||||
|
const existing = await trx
|
||||||
|
.select()
|
||||||
|
.from(aiBudgets)
|
||||||
|
.where(
|
||||||
|
input.scope === "public"
|
||||||
|
? eq(aiBudgets.resourceId, input.resourceId)
|
||||||
|
: eq(aiBudgets.siteResourceId, input.siteResourceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingByKey = new Map(
|
||||||
|
existing.map((b) => [`${b.unit}::${b.period}`, b])
|
||||||
|
);
|
||||||
|
|
||||||
|
const seenKeys = new Set<string>();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (const budget of budgets) {
|
||||||
|
const key = `${budget.unit}::${budget.period}`;
|
||||||
|
seenKeys.add(key);
|
||||||
|
const existingBudget = existingByKey.get(key);
|
||||||
|
|
||||||
|
if (existingBudget) {
|
||||||
|
await trx
|
||||||
|
.update(aiBudgets)
|
||||||
|
.set({
|
||||||
|
amount: budget.amount,
|
||||||
|
enforcement: budget.enforcement,
|
||||||
|
enabled: budget.enabled,
|
||||||
|
updatedAt: now
|
||||||
|
})
|
||||||
|
.where(eq(aiBudgets.budgetId, existingBudget.budgetId));
|
||||||
|
} else {
|
||||||
|
await trx.insert(aiBudgets).values({
|
||||||
|
orgId,
|
||||||
|
resourceId: input.scope === "public" ? input.resourceId : null,
|
||||||
|
siteResourceId:
|
||||||
|
input.scope === "site" ? input.siteResourceId : null,
|
||||||
|
amount: budget.amount,
|
||||||
|
unit: budget.unit,
|
||||||
|
period: budget.period,
|
||||||
|
enforcement: budget.enforcement,
|
||||||
|
enabled: budget.enabled,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, existingBudget] of existingByKey) {
|
||||||
|
if (!seenKeys.has(key)) {
|
||||||
|
await trx
|
||||||
|
.delete(aiBudgets)
|
||||||
|
.where(eq(aiBudgets.budgetId, existingBudget.budgetId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
aiModels,
|
||||||
|
aiProviders,
|
||||||
|
resourceAiModels,
|
||||||
|
siteResourceAiModels,
|
||||||
|
Transaction
|
||||||
|
} from "@server/db";
|
||||||
|
import {
|
||||||
|
AccessMode,
|
||||||
|
ModelListType,
|
||||||
|
clearPublicResourceAiConfig,
|
||||||
|
clearSiteResourceAiConfig,
|
||||||
|
isInferenceFieldsError,
|
||||||
|
resolveProviderAttachments,
|
||||||
|
setPublicResourceAiProviders,
|
||||||
|
setSiteResourceAiProviders
|
||||||
|
} from "@server/lib/aiInferenceResource";
|
||||||
|
|
||||||
|
export type BlueprintAiModelInput = {
|
||||||
|
model: string;
|
||||||
|
listType: ModelListType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BlueprintAiProviderInput = {
|
||||||
|
provider: string;
|
||||||
|
accessMode: AccessMode;
|
||||||
|
enabled: boolean;
|
||||||
|
models: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
async function resolveProviderNiceIds(
|
||||||
|
orgId: string,
|
||||||
|
niceIds: string[],
|
||||||
|
trx: Transaction
|
||||||
|
): Promise<Map<string, number>> {
|
||||||
|
const unique = [...new Set(niceIds)];
|
||||||
|
if (unique.length === 0) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await trx
|
||||||
|
.select({
|
||||||
|
providerId: aiProviders.providerId,
|
||||||
|
niceId: aiProviders.niceId
|
||||||
|
})
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.orgId, orgId),
|
||||||
|
inArray(aiProviders.niceId, unique)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const byNiceId = new Map(rows.map((r) => [r.niceId, r.providerId]));
|
||||||
|
const missing = unique.filter((id) => !byNiceId.has(id));
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`AI provider(s) not found in this org: ${missing.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return byNiceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveModelKeys(
|
||||||
|
providers: BlueprintAiProviderInput[],
|
||||||
|
providerIdByNiceId: Map<string, number>,
|
||||||
|
trx: Transaction
|
||||||
|
): Promise<Map<string, number>> {
|
||||||
|
const providerIds = [
|
||||||
|
...new Set(
|
||||||
|
providers
|
||||||
|
.filter((p) => p.models.length > 0)
|
||||||
|
.map((p) => providerIdByNiceId.get(p.provider)!)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (providerIds.length === 0) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await trx
|
||||||
|
.select({
|
||||||
|
modelId: aiModels.modelId,
|
||||||
|
modelKey: aiModels.modelKey,
|
||||||
|
providerId: aiModels.providerId
|
||||||
|
})
|
||||||
|
.from(aiModels)
|
||||||
|
.where(inArray(aiModels.providerId, providerIds));
|
||||||
|
|
||||||
|
const byProviderAndKey = new Map<string, number>();
|
||||||
|
for (const row of rows) {
|
||||||
|
byProviderAndKey.set(`${row.providerId}::${row.modelKey}`, row.modelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelIdByEntryKey = new Map<string, number>();
|
||||||
|
const missing: string[] = [];
|
||||||
|
for (const provider of providers) {
|
||||||
|
const providerId = providerIdByNiceId.get(provider.provider)!;
|
||||||
|
for (const m of provider.models) {
|
||||||
|
const modelId = byProviderAndKey.get(`${providerId}::${m}`);
|
||||||
|
if (modelId === undefined) {
|
||||||
|
missing.push(`${provider.provider}/${m}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
modelIdByEntryKey.set(`${provider.provider}::${m}`, modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(`AI model(s) not found: ${missing.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelIdByEntryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateModelEntries(input: {
|
||||||
|
orgId: string;
|
||||||
|
entries: { modelId: number }[];
|
||||||
|
selectProviderIds: number[];
|
||||||
|
trx: Transaction;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (input.entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.selectProviderIds.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
"Set at least one attached AI provider to access-mode 'select' before declaring models"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelIds = input.entries.map((e) => e.modelId);
|
||||||
|
const catalogRows = await input.trx
|
||||||
|
.select({
|
||||||
|
modelId: aiModels.modelId,
|
||||||
|
listType: aiModels.listType,
|
||||||
|
providerId: aiModels.providerId,
|
||||||
|
enabled: aiModels.enabled
|
||||||
|
})
|
||||||
|
.from(aiModels)
|
||||||
|
.innerJoin(aiProviders, eq(aiModels.providerId, aiProviders.providerId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(aiModels.modelId, modelIds),
|
||||||
|
inArray(aiModels.providerId, input.selectProviderIds),
|
||||||
|
eq(aiProviders.orgId, input.orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const catalogById = new Map(catalogRows.map((row) => [row.modelId, row]));
|
||||||
|
for (const entry of input.entries) {
|
||||||
|
const catalog = catalogById.get(entry.modelId);
|
||||||
|
if (!catalog) {
|
||||||
|
throw new Error(
|
||||||
|
`Model ${entry.modelId} does not exist or does not belong to a select-mode attached provider`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!catalog.enabled) {
|
||||||
|
throw new Error(
|
||||||
|
`Model ${entry.modelId} is disabled on its provider`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyncInferenceAiConfigInput = {
|
||||||
|
orgId: string;
|
||||||
|
trx: Transaction;
|
||||||
|
mode: string;
|
||||||
|
providers: BlueprintAiProviderInput[];
|
||||||
|
} & (
|
||||||
|
| { scope: "public"; resourceId: number }
|
||||||
|
| { scope: "site"; siteResourceId: number }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully declarative: makes the resource's attached AI providers/models match
|
||||||
|
* exactly what the blueprint declares (omitted providers/models are removed).
|
||||||
|
* Non-inference resources have any leftover AI config cleared.
|
||||||
|
*/
|
||||||
|
export async function syncInferenceAiConfig(
|
||||||
|
input: SyncInferenceAiConfigInput
|
||||||
|
): Promise<void> {
|
||||||
|
const { orgId, trx, mode } = input;
|
||||||
|
|
||||||
|
if (mode !== "inference") {
|
||||||
|
if (input.scope === "public") {
|
||||||
|
await clearPublicResourceAiConfig(input.resourceId, trx);
|
||||||
|
} else {
|
||||||
|
await clearSiteResourceAiConfig(input.siteResourceId, trx);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerIdByNiceId = await resolveProviderNiceIds(
|
||||||
|
orgId,
|
||||||
|
input.providers.map((p) => p.provider),
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvedAttachments = await resolveProviderAttachments({
|
||||||
|
orgId,
|
||||||
|
attachments: input.providers.map((p) => ({
|
||||||
|
providerId: providerIdByNiceId.get(p.provider)!,
|
||||||
|
accessMode: p.accessMode,
|
||||||
|
enabled: p.enabled
|
||||||
|
})),
|
||||||
|
requireAtLeastOne: false
|
||||||
|
});
|
||||||
|
if (isInferenceFieldsError(resolvedAttachments)) {
|
||||||
|
throw new Error(resolvedAttachments.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.scope === "public") {
|
||||||
|
await setPublicResourceAiProviders(
|
||||||
|
input.resourceId,
|
||||||
|
resolvedAttachments,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await setSiteResourceAiProviders(
|
||||||
|
input.siteResourceId,
|
||||||
|
resolvedAttachments,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelIdByEntryKey = await resolveModelKeys(
|
||||||
|
input.providers,
|
||||||
|
providerIdByNiceId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
|
||||||
|
const modelEntries = input.providers.flatMap((p) =>
|
||||||
|
p.models.map((m) => ({
|
||||||
|
modelId: modelIdByEntryKey.get(`${p.provider}::${m}`)!
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectProviderIds = resolvedAttachments
|
||||||
|
.filter((a) => a.accessMode === "select")
|
||||||
|
.map((a) => a.providerId);
|
||||||
|
|
||||||
|
await validateModelEntries({
|
||||||
|
orgId,
|
||||||
|
entries: modelEntries,
|
||||||
|
selectProviderIds,
|
||||||
|
trx
|
||||||
|
});
|
||||||
|
|
||||||
|
if (input.scope === "public") {
|
||||||
|
await trx
|
||||||
|
.delete(resourceAiModels)
|
||||||
|
.where(eq(resourceAiModels.resourceId, input.resourceId));
|
||||||
|
if (modelEntries.length > 0) {
|
||||||
|
await trx.insert(resourceAiModels).values(
|
||||||
|
modelEntries.map((m) => ({
|
||||||
|
resourceId: input.resourceId,
|
||||||
|
modelId: m.modelId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await trx
|
||||||
|
.delete(siteResourceAiModels)
|
||||||
|
.where(
|
||||||
|
eq(siteResourceAiModels.siteResourceId, input.siteResourceId)
|
||||||
|
);
|
||||||
|
if (modelEntries.length > 0) {
|
||||||
|
await trx.insert(siteResourceAiModels).values(
|
||||||
|
modelEntries.map((m) => ({
|
||||||
|
siteResourceId: input.siteResourceId,
|
||||||
|
modelId: m.modelId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
labels,
|
||||||
|
resourceLabels,
|
||||||
|
siteResourceLabels,
|
||||||
|
Transaction
|
||||||
|
} from "@server/db";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
// Matches the "gray" swatch in the label color palette used by the UI
|
||||||
|
// (src/components/labels-selector.tsx), used as the default for labels
|
||||||
|
// auto-created from a blueprint where no color is specified.
|
||||||
|
const DEFAULT_LABEL_COLOR = "#b4b4b4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up labels by name (case-insensitive) within an org, auto-creating
|
||||||
|
* any that don't already exist. Returns the resolved, de-duplicated labelIds.
|
||||||
|
*/
|
||||||
|
export async function getOrCreateLabelIds(
|
||||||
|
orgId: string,
|
||||||
|
labelNames: string[],
|
||||||
|
trx: Transaction
|
||||||
|
): Promise<number[]> {
|
||||||
|
const labelIds = new Set<number>();
|
||||||
|
|
||||||
|
for (const name of labelNames) {
|
||||||
|
let [label] = await trx
|
||||||
|
.select({ labelId: labels.labelId })
|
||||||
|
.from(labels)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(labels.orgId, orgId),
|
||||||
|
sql`LOWER(${labels.name}) = ${name.toLowerCase()}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
[label] = await trx
|
||||||
|
.insert(labels)
|
||||||
|
.values({ name, color: DEFAULT_LABEL_COLOR, orgId })
|
||||||
|
.returning({ labelId: labels.labelId });
|
||||||
|
logger.info(
|
||||||
|
`Auto-created label "${name}" in org ${orgId} from blueprint`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
labelIds.add(label.labelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(labelIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncResourceLabels(
|
||||||
|
resourceId: number,
|
||||||
|
labelIds: number[],
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
await trx
|
||||||
|
.delete(resourceLabels)
|
||||||
|
.where(eq(resourceLabels.resourceId, resourceId));
|
||||||
|
|
||||||
|
if (labelIds.length > 0) {
|
||||||
|
await trx
|
||||||
|
.insert(resourceLabels)
|
||||||
|
.values(labelIds.map((labelId) => ({ resourceId, labelId })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncSiteResourceLabels(
|
||||||
|
siteResourceId: number,
|
||||||
|
labelIds: number[],
|
||||||
|
trx: Transaction
|
||||||
|
) {
|
||||||
|
await trx
|
||||||
|
.delete(siteResourceLabels)
|
||||||
|
.where(eq(siteResourceLabels.siteResourceId, siteResourceId));
|
||||||
|
|
||||||
|
if (labelIds.length > 0) {
|
||||||
|
await trx
|
||||||
|
.insert(siteResourceLabels)
|
||||||
|
.values(
|
||||||
|
labelIds.map((labelId) => ({ siteResourceId, labelId }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,20 +19,22 @@ import {
|
|||||||
import { sites } from "@server/db";
|
import { sites } from "@server/db";
|
||||||
import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm";
|
import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm";
|
||||||
import { Config } from "./types";
|
import { Config } from "./types";
|
||||||
|
import { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
||||||
import { getNextAvailableAliasAddress } from "../ip";
|
import { getNextAvailableAliasAddress } from "../ip";
|
||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "@server/routers/certificates/createCertificate";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
|
||||||
import { tierMatrix } from "../billing/tierMatrix";
|
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { LimitId } from "../billing";
|
import { LimitId } from "../billing";
|
||||||
import { usageService } from "../billing/usageService";
|
import { usageService } from "../billing/usageService";
|
||||||
|
import { syncInferenceAiConfig } from "./aiProviders";
|
||||||
|
import { syncAiBudgets } from "./aiBudgets";
|
||||||
|
|
||||||
async function getDomainForSiteResource(
|
async function getDomainForSiteResource(
|
||||||
siteResourceId: number | undefined,
|
siteResourceId: number | undefined,
|
||||||
fullDomain: string,
|
fullDomain: string,
|
||||||
orgId: string,
|
orgId: string,
|
||||||
|
isInference: boolean,
|
||||||
trx: Transaction
|
trx: Transaction
|
||||||
): Promise<{ subdomain: string | null; domainId: string }> {
|
): Promise<{ subdomain: string | null; domainId: string }> {
|
||||||
const [fullDomainExists] = await trx
|
const [fullDomainExists] = await trx
|
||||||
@@ -42,6 +44,11 @@ async function getDomainForSiteResource(
|
|||||||
and(
|
and(
|
||||||
eq(siteResources.fullDomain, fullDomain),
|
eq(siteResources.fullDomain, fullDomain),
|
||||||
eq(siteResources.orgId, orgId),
|
eq(siteResources.orgId, orgId),
|
||||||
|
// exclude looking at the ones on exit nodes if this is an inference resource,
|
||||||
|
// and vice versa, so inference and non-inference resources can share a full-domain
|
||||||
|
isInference
|
||||||
|
? ne(siteResources.mode, "inference")
|
||||||
|
: eq(siteResources.mode, "inference"),
|
||||||
siteResourceId
|
siteResourceId
|
||||||
? ne(siteResources.siteResourceId, siteResourceId)
|
? ne(siteResources.siteResourceId, siteResourceId)
|
||||||
: isNotNull(siteResources.siteResourceId)
|
: isNotNull(siteResources.siteResourceId)
|
||||||
@@ -119,30 +126,6 @@ export async function updatePrivateResources(
|
|||||||
for (const [resourceNiceId, resourceData] of Object.entries(
|
for (const [resourceNiceId, resourceData] of Object.entries(
|
||||||
config["client-resources"]
|
config["client-resources"]
|
||||||
)) {
|
)) {
|
||||||
if (resourceData.mode === "http") {
|
|
||||||
const hasHttpFeature = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.advancedPrivateResources
|
|
||||||
);
|
|
||||||
if (!hasHttpFeature) {
|
|
||||||
throw new Error(
|
|
||||||
"HTTP private resources are not included in your current plan. Please upgrade."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resourceData.mode === "ssh") {
|
|
||||||
const hasSshFeature = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.advancedPrivateResources
|
|
||||||
);
|
|
||||||
if (!hasSshFeature) {
|
|
||||||
throw new Error(
|
|
||||||
"SSH private resources are not included in your current plan. Please upgrade."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [existingResource] = await trx
|
const [existingResource] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
@@ -213,7 +196,7 @@ export async function updatePrivateResources(
|
|||||||
resourceStatusFromSite = siteSingle.status ?? "approved";
|
resourceStatusFromSite = siteSingle.status ?? "approved";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allSites.length === 0) {
|
if (resourceData.mode !== "inference" && allSites.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No valid sites found for private private resource ${resourceNiceId} in org ${orgId}`
|
`No valid sites found for private private resource ${resourceNiceId} in org ${orgId}`
|
||||||
);
|
);
|
||||||
@@ -226,15 +209,27 @@ export async function updatePrivateResources(
|
|||||||
? false
|
? false
|
||||||
: resourceData.enabled;
|
: resourceData.enabled;
|
||||||
|
|
||||||
|
const resourceSsl =
|
||||||
|
resourceData.mode === "inference" || resourceData.mode === "http"
|
||||||
|
? resourceData.ssl == undefined || resourceData.ssl == null
|
||||||
|
? true
|
||||||
|
: resourceData.ssl
|
||||||
|
: resourceData.ssl;
|
||||||
|
|
||||||
if (existingResource) {
|
if (existingResource) {
|
||||||
let domainInfo:
|
let domainInfo:
|
||||||
| { subdomain: string | null; domainId: string }
|
| { subdomain: string | null; domainId: string }
|
||||||
| undefined;
|
| undefined;
|
||||||
if (resourceData["full-domain"] && resourceData.mode === "http") {
|
if (
|
||||||
|
resourceData["full-domain"] &&
|
||||||
|
(resourceData.mode === "http" ||
|
||||||
|
resourceData.mode === "inference")
|
||||||
|
) {
|
||||||
domainInfo = await getDomainForSiteResource(
|
domainInfo = await getDomainForSiteResource(
|
||||||
existingResource.siteResourceId,
|
existingResource.siteResourceId,
|
||||||
resourceData["full-domain"],
|
resourceData["full-domain"],
|
||||||
orgId,
|
orgId,
|
||||||
|
resourceData.mode === "inference",
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -264,13 +259,15 @@ export async function updatePrivateResources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInference = resourceData.mode === "inference";
|
||||||
|
|
||||||
// Update existing resource
|
// Update existing resource
|
||||||
const [updatedResource] = await trx
|
const [updatedResource] = await trx
|
||||||
.update(siteResources)
|
.update(siteResources)
|
||||||
.set({
|
.set({
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
mode: resourceData.mode,
|
mode: resourceData.mode,
|
||||||
ssl: resourceData.ssl,
|
ssl: resourceSsl,
|
||||||
scheme: resourceData.scheme,
|
scheme: resourceData.scheme,
|
||||||
destination: resourceData.destination,
|
destination: resourceData.destination,
|
||||||
destinationPort: resourceData["destination-port"],
|
destinationPort: resourceData["destination-port"],
|
||||||
@@ -278,13 +275,15 @@ export async function updatePrivateResources(
|
|||||||
alias: resourceData.alias || null,
|
alias: resourceData.alias || null,
|
||||||
disableIcmp:
|
disableIcmp:
|
||||||
resourceData["disable-icmp"] ||
|
resourceData["disable-icmp"] ||
|
||||||
(resourceData.mode == "http" ? true : false), // default to true for http resources, otherwise false
|
(resourceData.mode == "http" || isInference
|
||||||
|
? true
|
||||||
|
: false), // default to true for http/inference resources, otherwise false
|
||||||
tcpPortRangeString:
|
tcpPortRangeString:
|
||||||
resourceData.mode == "http"
|
resourceData.mode == "http" || isInference
|
||||||
? "443,80"
|
? "443,80"
|
||||||
: resourceData["tcp-ports"],
|
: resourceData["tcp-ports"],
|
||||||
udpPortRangeString:
|
udpPortRangeString:
|
||||||
resourceData.mode == "http"
|
resourceData.mode == "http" || isInference
|
||||||
? ""
|
? ""
|
||||||
: resourceData["udp-ports"],
|
: resourceData["udp-ports"],
|
||||||
fullDomain: resourceData["full-domain"] || null,
|
fullDomain: resourceData["full-domain"] || null,
|
||||||
@@ -294,7 +293,9 @@ export async function updatePrivateResources(
|
|||||||
authDaemonMode:
|
authDaemonMode:
|
||||||
resourceData["auth-daemon"]?.mode || "native",
|
resourceData["auth-daemon"]?.mode || "native",
|
||||||
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
|
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
|
||||||
status: resourceStatusFromSite
|
status: resourceStatusFromSite,
|
||||||
|
networkId: isInference ? null : undefined,
|
||||||
|
requiresExitNodeConnection: isInference
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
eq(
|
eq(
|
||||||
@@ -306,7 +307,19 @@ export async function updatePrivateResources(
|
|||||||
|
|
||||||
const siteResourceId = existingResource.siteResourceId;
|
const siteResourceId = existingResource.siteResourceId;
|
||||||
|
|
||||||
if (updatedResource.networkId) {
|
if (isInference) {
|
||||||
|
// inference resources are not attached to any site network
|
||||||
|
if (existingResource.networkId) {
|
||||||
|
await trx
|
||||||
|
.delete(siteNetworks)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
siteNetworks.networkId,
|
||||||
|
existingResource.networkId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (updatedResource.networkId) {
|
||||||
await trx
|
await trx
|
||||||
.delete(siteNetworks)
|
.delete(siteNetworks)
|
||||||
.where(
|
.where(
|
||||||
@@ -321,6 +334,28 @@ export async function updatePrivateResources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await syncInferenceAiConfig({
|
||||||
|
orgId,
|
||||||
|
trx,
|
||||||
|
mode: resourceData.mode,
|
||||||
|
scope: "site",
|
||||||
|
siteResourceId,
|
||||||
|
providers: resourceData["ai-providers"].map((p) => ({
|
||||||
|
provider: p.provider,
|
||||||
|
accessMode: p["access-mode"],
|
||||||
|
enabled: p.enabled,
|
||||||
|
models: p.models
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncAiBudgets({
|
||||||
|
orgId,
|
||||||
|
trx,
|
||||||
|
scope: "site",
|
||||||
|
siteResourceId,
|
||||||
|
budgets: resourceData["ai-budget"]
|
||||||
|
});
|
||||||
|
|
||||||
await trx
|
await trx
|
||||||
.delete(clientSiteResources)
|
.delete(clientSiteResources)
|
||||||
.where(eq(clientSiteResources.siteResourceId, siteResourceId));
|
.where(eq(clientSiteResources.siteResourceId, siteResourceId));
|
||||||
@@ -443,6 +478,13 @@ export async function updatePrivateResources(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const labelIds = await getOrCreateLabelIds(
|
||||||
|
orgId,
|
||||||
|
resourceData.labels,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
await syncSiteResourceLabels(siteResourceId, labelIds, trx);
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
newSiteResource: updatedResource,
|
newSiteResource: updatedResource,
|
||||||
oldSiteResource: existingResource,
|
oldSiteResource: existingResource,
|
||||||
@@ -493,14 +535,20 @@ export async function updatePrivateResources(
|
|||||||
releaseAliasLock = release;
|
releaseAliasLock = release;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInference = resourceData.mode === "inference";
|
||||||
|
|
||||||
let domainInfo:
|
let domainInfo:
|
||||||
| { subdomain: string | null; domainId: string }
|
| { subdomain: string | null; domainId: string }
|
||||||
| undefined;
|
| undefined;
|
||||||
if (resourceData["full-domain"] && resourceData.mode === "http") {
|
if (
|
||||||
|
resourceData["full-domain"] &&
|
||||||
|
(resourceData.mode === "http" || isInference)
|
||||||
|
) {
|
||||||
domainInfo = await getDomainForSiteResource(
|
domainInfo = await getDomainForSiteResource(
|
||||||
undefined,
|
undefined,
|
||||||
resourceData["full-domain"],
|
resourceData["full-domain"],
|
||||||
orgId,
|
orgId,
|
||||||
|
isInference,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -526,13 +574,16 @@ export async function updatePrivateResources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [network] = await trx
|
let network: typeof networks.$inferSelect | undefined;
|
||||||
|
if (!isInference) {
|
||||||
|
[network] = await trx
|
||||||
.insert(networks)
|
.insert(networks)
|
||||||
.values({
|
.values({
|
||||||
scope: "resource",
|
scope: "resource",
|
||||||
orgId: orgId
|
orgId: orgId
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
}
|
||||||
|
|
||||||
// Create new resource
|
// Create new resource
|
||||||
const [newResource] = await trx
|
const [newResource] = await trx
|
||||||
@@ -540,11 +591,11 @@ export async function updatePrivateResources(
|
|||||||
.values({
|
.values({
|
||||||
orgId: orgId,
|
orgId: orgId,
|
||||||
niceId: resourceNiceId,
|
niceId: resourceNiceId,
|
||||||
networkId: network.networkId,
|
networkId: network ? network.networkId : null,
|
||||||
defaultNetworkId: network.networkId,
|
defaultNetworkId: network ? network.networkId : null,
|
||||||
name: resourceData.name || resourceNiceId,
|
name: resourceData.name || resourceNiceId,
|
||||||
mode: resourceData.mode,
|
mode: resourceData.mode,
|
||||||
ssl: resourceData.ssl,
|
ssl: resourceSsl,
|
||||||
scheme: resourceData.scheme,
|
scheme: resourceData.scheme,
|
||||||
destination: resourceData.destination,
|
destination: resourceData.destination,
|
||||||
destinationPort: resourceData["destination-port"],
|
destinationPort: resourceData["destination-port"],
|
||||||
@@ -553,13 +604,15 @@ export async function updatePrivateResources(
|
|||||||
aliasAddress: aliasAddress,
|
aliasAddress: aliasAddress,
|
||||||
disableIcmp:
|
disableIcmp:
|
||||||
resourceData["disable-icmp"] ||
|
resourceData["disable-icmp"] ||
|
||||||
(resourceData.mode == "http" ? true : false), // default to true for http resources, otherwise false
|
(resourceData.mode == "http" || isInference
|
||||||
|
? true
|
||||||
|
: false), // default to true for http/inference resources, otherwise false
|
||||||
tcpPortRangeString:
|
tcpPortRangeString:
|
||||||
resourceData.mode == "http"
|
resourceData.mode == "http" || isInference
|
||||||
? "443,80"
|
? "443,80"
|
||||||
: resourceData["tcp-ports"],
|
: resourceData["tcp-ports"],
|
||||||
udpPortRangeString:
|
udpPortRangeString:
|
||||||
resourceData.mode == "http"
|
resourceData.mode == "http" || isInference
|
||||||
? ""
|
? ""
|
||||||
: resourceData["udp-ports"],
|
: resourceData["udp-ports"],
|
||||||
fullDomain: resourceData["full-domain"] || null,
|
fullDomain: resourceData["full-domain"] || null,
|
||||||
@@ -569,7 +622,8 @@ export async function updatePrivateResources(
|
|||||||
authDaemonMode:
|
authDaemonMode:
|
||||||
resourceData["auth-daemon"]?.mode || "native",
|
resourceData["auth-daemon"]?.mode || "native",
|
||||||
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
|
authDaemonPort: resourceData["auth-daemon"]?.port || 22123,
|
||||||
status: resourceStatusFromSite
|
status: resourceStatusFromSite,
|
||||||
|
requiresExitNodeConnection: isInference
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -577,12 +631,36 @@ export async function updatePrivateResources(
|
|||||||
|
|
||||||
const siteResourceId = newResource.siteResourceId;
|
const siteResourceId = newResource.siteResourceId;
|
||||||
|
|
||||||
|
if (network) {
|
||||||
for (const site of allSites) {
|
for (const site of allSites) {
|
||||||
await trx.insert(siteNetworks).values({
|
await trx.insert(siteNetworks).values({
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
networkId: network.networkId
|
networkId: network.networkId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncInferenceAiConfig({
|
||||||
|
orgId,
|
||||||
|
trx,
|
||||||
|
mode: resourceData.mode,
|
||||||
|
scope: "site",
|
||||||
|
siteResourceId,
|
||||||
|
providers: resourceData["ai-providers"].map((p) => ({
|
||||||
|
provider: p.provider,
|
||||||
|
accessMode: p["access-mode"],
|
||||||
|
enabled: p.enabled,
|
||||||
|
models: p.models
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncAiBudgets({
|
||||||
|
orgId,
|
||||||
|
trx,
|
||||||
|
scope: "site",
|
||||||
|
siteResourceId,
|
||||||
|
budgets: resourceData["ai-budget"]
|
||||||
|
});
|
||||||
|
|
||||||
const [adminRole] = await trx
|
const [adminRole] = await trx
|
||||||
.select()
|
.select()
|
||||||
@@ -697,6 +775,13 @@ export async function updatePrivateResources(
|
|||||||
|
|
||||||
await usageService.add(orgId, LimitId.PRIVATE_RESOURCES, 1, trx);
|
await usageService.add(orgId, LimitId.PRIVATE_RESOURCES, 1, trx);
|
||||||
|
|
||||||
|
const labelIds = await getOrCreateLabelIds(
|
||||||
|
orgId,
|
||||||
|
resourceData.labels,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
await syncSiteResourceLabels(siteResourceId, labelIds, trx);
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
newSiteResource: newResource,
|
newSiteResource: newResource,
|
||||||
newSites: allSites,
|
newSites: allSites,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "@server/routers/certificates/createCertificate";
|
||||||
import { hashPassword } from "@server/auth/password";
|
import { hashPassword } from "@server/auth/password";
|
||||||
import { generateId } from "@server/auth/sessions/app";
|
import { generateId } from "@server/auth/sessions/app";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
@@ -50,11 +50,11 @@ import { and, asc, eq, isNotNull, ne, or } from "drizzle-orm";
|
|||||||
import { tierMatrix } from "../billing/tierMatrix";
|
import { tierMatrix } from "../billing/tierMatrix";
|
||||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import { Config, isTargetsOnlyResource, TargetData } from "./types";
|
import { Config, isTargetsOnlyResource, TargetData } from "./types";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import { getOrCreateLabelIds, syncResourceLabels } from "./labels";
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import next from "next";
|
|
||||||
import { LimitId } from "../billing";
|
import { LimitId } from "../billing";
|
||||||
import { usageService } from "../billing/usageService";
|
import { usageService } from "../billing/usageService";
|
||||||
|
import { syncInferenceAiConfig } from "./aiProviders";
|
||||||
|
import { syncAiBudgets } from "./aiBudgets";
|
||||||
|
|
||||||
export type PublicResourcesResults = {
|
export type PublicResourcesResults = {
|
||||||
proxyResource: Resource;
|
proxyResource: Resource;
|
||||||
@@ -259,18 +259,6 @@ export async function updatePublicResources(
|
|||||||
headers = JSON.stringify(resourceData.headers);
|
headers = JSON.stringify(resourceData.headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["ssh", "rdp", "vnc"].includes(resourceData.mode || "")) {
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.advancedPublicResources
|
|
||||||
);
|
|
||||||
if (!isLicensed) {
|
|
||||||
throw new Error(
|
|
||||||
"Your current subscription does not support browser gateway resources. Please upgrade to access this feature."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resourceData.policy) {
|
if (resourceData.policy) {
|
||||||
const isLicensed = await isLicensedOrSubscribed(
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
orgId,
|
orgId,
|
||||||
@@ -286,7 +274,9 @@ export async function updatePublicResources(
|
|||||||
if (existingResource) {
|
if (existingResource) {
|
||||||
let domain;
|
let domain;
|
||||||
if (
|
if (
|
||||||
["http", "ssh", "rdp", "vnc"].includes(resourceData.mode || "")
|
["http", "ssh", "rdp", "vnc", "inference"].includes(
|
||||||
|
resourceData.mode || ""
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if (resourceData["full-domain"]?.startsWith("*.")) {
|
if (resourceData["full-domain"]?.startsWith("*.")) {
|
||||||
const isLicensed = await isLicensedOrSubscribed(
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
@@ -304,6 +294,7 @@ export async function updatePublicResources(
|
|||||||
existingResource.resourceId,
|
existingResource.resourceId,
|
||||||
resourceData["full-domain"]!,
|
resourceData["full-domain"]!,
|
||||||
orgId,
|
orgId,
|
||||||
|
resourceData.mode === "inference",
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -325,7 +316,7 @@ export async function updatePublicResources(
|
|||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
orgId,
|
orgId,
|
||||||
tierMatrix.maintencePage
|
tierMatrix.maintenancePage
|
||||||
);
|
);
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
resourceData.maintenance = undefined;
|
resourceData.maintenance = undefined;
|
||||||
@@ -370,14 +361,22 @@ export async function updatePublicResources(
|
|||||||
name: resourceData.name || "Unnamed Resource",
|
name: resourceData.name || "Unnamed Resource",
|
||||||
|
|
||||||
mode: resourceData.mode,
|
mode: resourceData.mode,
|
||||||
proxyPort: ["http", "ssh", "rdp", "vnc"].includes(
|
proxyPort: [
|
||||||
resourceData.mode || ""
|
"http",
|
||||||
)
|
"ssh",
|
||||||
|
"rdp",
|
||||||
|
"vnc",
|
||||||
|
"inference"
|
||||||
|
].includes(resourceData.mode || "")
|
||||||
? null
|
? null
|
||||||
: resourceData["proxy-port"],
|
: resourceData["proxy-port"],
|
||||||
fullDomain: ["http", "ssh", "rdp", "vnc"].includes(
|
fullDomain: [
|
||||||
resourceData.mode || ""
|
"http",
|
||||||
)
|
"ssh",
|
||||||
|
"rdp",
|
||||||
|
"vnc",
|
||||||
|
"inference"
|
||||||
|
].includes(resourceData.mode || "")
|
||||||
? resourceData["full-domain"]
|
? resourceData["full-domain"]
|
||||||
: null,
|
: null,
|
||||||
subdomain: domain ? domain.subdomain : null,
|
subdomain: domain ? domain.subdomain : null,
|
||||||
@@ -566,14 +565,23 @@ export async function updatePublicResources(
|
|||||||
.update(resources)
|
.update(resources)
|
||||||
.set({
|
.set({
|
||||||
name: resourceData.name || "Unnamed Resource",
|
name: resourceData.name || "Unnamed Resource",
|
||||||
proxyPort: ["http", "ssh", "rdp", "vnc"].includes(
|
mode: resourceData.mode,
|
||||||
resourceData.mode || ""
|
proxyPort: [
|
||||||
)
|
"http",
|
||||||
|
"ssh",
|
||||||
|
"rdp",
|
||||||
|
"vnc",
|
||||||
|
"inference"
|
||||||
|
].includes(resourceData.mode || "")
|
||||||
? null
|
? null
|
||||||
: resourceData["proxy-port"],
|
: resourceData["proxy-port"],
|
||||||
fullDomain: ["http", "ssh", "rdp", "vnc"].includes(
|
fullDomain: [
|
||||||
resourceData.mode || ""
|
"http",
|
||||||
)
|
"ssh",
|
||||||
|
"rdp",
|
||||||
|
"vnc",
|
||||||
|
"inference"
|
||||||
|
].includes(resourceData.mode || "")
|
||||||
? resourceData["full-domain"]
|
? resourceData["full-domain"]
|
||||||
: null,
|
: null,
|
||||||
subdomain: domain ? domain.subdomain : null,
|
subdomain: domain ? domain.subdomain : null,
|
||||||
@@ -673,6 +681,30 @@ export async function updatePublicResources(
|
|||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await syncInferenceAiConfig({
|
||||||
|
orgId,
|
||||||
|
trx,
|
||||||
|
mode: resourceData.mode || "",
|
||||||
|
scope: "public",
|
||||||
|
resourceId: existingResource.resourceId,
|
||||||
|
providers: (resourceData["ai-providers"] || []).map(
|
||||||
|
(p) => ({
|
||||||
|
provider: p.provider,
|
||||||
|
accessMode: p["access-mode"],
|
||||||
|
enabled: p.enabled,
|
||||||
|
models: p.models
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncAiBudgets({
|
||||||
|
orgId,
|
||||||
|
trx,
|
||||||
|
scope: "public",
|
||||||
|
resourceId: existingResource.resourceId,
|
||||||
|
budgets: resourceData["ai-budget"] || []
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingResourceTargets = await trx
|
const existingResourceTargets = await trx
|
||||||
@@ -753,7 +785,7 @@ export async function updatePublicResources(
|
|||||||
: undefined),
|
: undefined),
|
||||||
rewritePathType: targetData["rewrite-match"],
|
rewritePathType: targetData["rewrite-match"],
|
||||||
priority: targetData.priority,
|
priority: targetData.priority,
|
||||||
mode: resourceData.mode
|
mode: resourceData.mode as Target["mode"]
|
||||||
})
|
})
|
||||||
.where(eq(targets.targetId, existingTarget.targetId))
|
.where(eq(targets.targetId, existingTarget.targetId))
|
||||||
.returning();
|
.returning();
|
||||||
@@ -1058,7 +1090,9 @@ export async function updatePublicResources(
|
|||||||
|
|
||||||
let domain;
|
let domain;
|
||||||
if (
|
if (
|
||||||
["http", "ssh", "rdp", "vnc"].includes(resourceData.mode || "")
|
["http", "ssh", "rdp", "vnc", "inference"].includes(
|
||||||
|
resourceData.mode || ""
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if (resourceData["full-domain"]?.startsWith("*.")) {
|
if (resourceData["full-domain"]?.startsWith("*.")) {
|
||||||
const isLicensed = await isLicensedOrSubscribed(
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
@@ -1076,6 +1110,7 @@ export async function updatePublicResources(
|
|||||||
undefined,
|
undefined,
|
||||||
resourceData["full-domain"]!,
|
resourceData["full-domain"]!,
|
||||||
orgId,
|
orgId,
|
||||||
|
resourceData.mode === "inference",
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1088,7 +1123,7 @@ export async function updatePublicResources(
|
|||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(
|
const isLicensed = await isLicensedOrSubscribed(
|
||||||
orgId,
|
orgId,
|
||||||
tierMatrix.maintencePage
|
tierMatrix.maintenancePage
|
||||||
);
|
);
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
resourceData.maintenance = undefined;
|
resourceData.maintenance = undefined;
|
||||||
@@ -1155,14 +1190,22 @@ export async function updatePublicResources(
|
|||||||
status: resourceStatusFromSite,
|
status: resourceStatusFromSite,
|
||||||
name: resourceData.name || "Unnamed Resource",
|
name: resourceData.name || "Unnamed Resource",
|
||||||
mode: resourceData.mode,
|
mode: resourceData.mode,
|
||||||
proxyPort: ["http", "ssh", "rdp", "vnc"].includes(
|
proxyPort: [
|
||||||
resourceData.mode || ""
|
"http",
|
||||||
)
|
"ssh",
|
||||||
|
"rdp",
|
||||||
|
"vnc",
|
||||||
|
"inference"
|
||||||
|
].includes(resourceData.mode || "")
|
||||||
? null
|
? null
|
||||||
: resourceData["proxy-port"],
|
: resourceData["proxy-port"],
|
||||||
fullDomain: ["http", "ssh", "rdp", "vnc"].includes(
|
fullDomain: [
|
||||||
resourceData.mode || ""
|
"http",
|
||||||
)
|
"ssh",
|
||||||
|
"rdp",
|
||||||
|
"vnc",
|
||||||
|
"inference"
|
||||||
|
].includes(resourceData.mode || "")
|
||||||
? resourceData["full-domain"]
|
? resourceData["full-domain"]
|
||||||
: null,
|
: null,
|
||||||
subdomain: domain ? domain.subdomain : null,
|
subdomain: domain ? domain.subdomain : null,
|
||||||
@@ -1217,6 +1260,28 @@ export async function updatePublicResources(
|
|||||||
|
|
||||||
resource = newResource;
|
resource = newResource;
|
||||||
|
|
||||||
|
await syncInferenceAiConfig({
|
||||||
|
orgId,
|
||||||
|
trx,
|
||||||
|
mode: resourceData.mode || "",
|
||||||
|
scope: "public",
|
||||||
|
resourceId: newResource.resourceId,
|
||||||
|
providers: (resourceData["ai-providers"] || []).map((p) => ({
|
||||||
|
provider: p.provider,
|
||||||
|
accessMode: p["access-mode"],
|
||||||
|
enabled: p.enabled,
|
||||||
|
models: p.models
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncAiBudgets({
|
||||||
|
orgId,
|
||||||
|
trx,
|
||||||
|
scope: "public",
|
||||||
|
resourceId: newResource.resourceId,
|
||||||
|
budgets: resourceData["ai-budget"] || []
|
||||||
|
});
|
||||||
|
|
||||||
await trx.insert(roleResources).values({
|
await trx.insert(roleResources).values({
|
||||||
roleId: adminRole.roleId,
|
roleId: adminRole.roleId,
|
||||||
resourceId: newResource.resourceId
|
resourceId: newResource.resourceId
|
||||||
@@ -1351,6 +1416,15 @@ export async function updatePublicResources(
|
|||||||
logger.debug(`Created resource ${newResource.resourceId}`);
|
logger.debug(`Created resource ${newResource.resourceId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isTargetsOnlyResource(resourceData)) {
|
||||||
|
const labelIds = await getOrCreateLabelIds(
|
||||||
|
orgId,
|
||||||
|
resourceData.labels || [],
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
await syncResourceLabels(resource.resourceId, labelIds, trx);
|
||||||
|
}
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
proxyResource: resource,
|
proxyResource: resource,
|
||||||
targetsToUpdate,
|
targetsToUpdate,
|
||||||
@@ -2065,6 +2139,7 @@ export async function getDomain(
|
|||||||
resourceId: number | undefined,
|
resourceId: number | undefined,
|
||||||
fullDomain: string,
|
fullDomain: string,
|
||||||
orgId: string,
|
orgId: string,
|
||||||
|
isInference: boolean,
|
||||||
trx: Transaction
|
trx: Transaction
|
||||||
) {
|
) {
|
||||||
const [fullDomainExists] = await trx
|
const [fullDomainExists] = await trx
|
||||||
@@ -2074,6 +2149,14 @@ export async function getDomain(
|
|||||||
and(
|
and(
|
||||||
eq(resources.fullDomain, fullDomain),
|
eq(resources.fullDomain, fullDomain),
|
||||||
eq(resources.orgId, orgId),
|
eq(resources.orgId, orgId),
|
||||||
|
// Inference resources route through the central AI gateway
|
||||||
|
// rather than normal target-based proxying, so they're
|
||||||
|
// allowed to share a full-domain with a non-inference
|
||||||
|
// resource (and vice versa) - only conflicts within the
|
||||||
|
// same routing category are rejected.
|
||||||
|
isInference
|
||||||
|
? ne(resources.mode, "inference")
|
||||||
|
: eq(resources.mode, "inference"),
|
||||||
resourceId
|
resourceId
|
||||||
? ne(resources.resourceId, resourceId)
|
? ne(resources.resourceId, resourceId)
|
||||||
: isNotNull(resources.resourceId)
|
: isNotNull(resources.resourceId)
|
||||||
|
|||||||
+131
-11
@@ -5,6 +5,11 @@ import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
|
|||||||
import { isValidRegionId } from "@server/db/regions";
|
import { isValidRegionId } from "@server/db/regions";
|
||||||
import { wildcardSubdomainSchema } from "@server/lib/schemas";
|
import { wildcardSubdomainSchema } from "@server/lib/schemas";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
import {
|
||||||
|
aiBudgetEnforcementSchema,
|
||||||
|
aiBudgetPeriodSchema,
|
||||||
|
aiBudgetUnitSchema
|
||||||
|
} from "@server/routers/aiBudget/validation";
|
||||||
|
|
||||||
const maxmindDbPath = config.getRawConfig().server.maxmind_db_path;
|
const maxmindDbPath = config.getRawConfig().server.maxmind_db_path;
|
||||||
const maxmindAsnPath = config.getRawConfig().server.maxmind_asn_path;
|
const maxmindAsnPath = config.getRawConfig().server.maxmind_asn_path;
|
||||||
@@ -183,6 +188,56 @@ export const HeaderSchema = z.object({
|
|||||||
value: z.string().min(1)
|
value: z.string().min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AiProviderAttachmentSchema = z
|
||||||
|
.object({
|
||||||
|
provider: z.string().min(1),
|
||||||
|
"access-mode": z
|
||||||
|
.enum(["inherit", "select"])
|
||||||
|
.optional()
|
||||||
|
.default("inherit"),
|
||||||
|
enabled: z.boolean().optional().default(true),
|
||||||
|
models: z.array(z.string()).optional().default([])
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(provider) => {
|
||||||
|
if (provider.models.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return provider["access-mode"] === "select";
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["models"],
|
||||||
|
error: "'models' can only be set on a provider with access-mode 'select'"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AiBudgetSchema = z.object({
|
||||||
|
amount: z.number().positive(),
|
||||||
|
unit: aiBudgetUnitSchema,
|
||||||
|
period: aiBudgetPeriodSchema.optional().default("monthly"),
|
||||||
|
enforcement: aiBudgetEnforcementSchema.optional().default("hard"),
|
||||||
|
enabled: z.boolean().optional().default(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
const aiBudgetArraySchema = z.array(AiBudgetSchema).refine(
|
||||||
|
(budgets) => {
|
||||||
|
const keys = budgets.map((b) => `${b.unit}::${b.period}`);
|
||||||
|
return keys.length === new Set(keys).size;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"'ai-budget' entries must not overlap: only one budget per unit/period combination is allowed"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// No default here: an object with only 'targets' set must remain
|
||||||
|
// recognized as a targets-only resource by isTargetsOnlyResource().
|
||||||
|
export const AiBudgetListSchema = aiBudgetArraySchema.optional();
|
||||||
|
|
||||||
|
export const AiBudgetListSchemaWithDefault = aiBudgetArraySchema
|
||||||
|
.optional()
|
||||||
|
.default([]);
|
||||||
|
|
||||||
export const AuthDaemonSchema = z
|
export const AuthDaemonSchema = z
|
||||||
.object({
|
.object({
|
||||||
pam: z.enum(["passthrough", "push"]).optional().default("passthrough"),
|
pam: z.enum(["passthrough", "push"]).optional().default("passthrough"),
|
||||||
@@ -209,7 +264,9 @@ export const PublicResourceSchema = z
|
|||||||
protocol: z
|
protocol: z
|
||||||
.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"])
|
.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"])
|
||||||
.optional(), // this was the old one and is now DEPRECATED in favor of the mode
|
.optional(), // this was the old one and is now DEPRECATED in favor of the mode
|
||||||
mode: z.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"]).optional(),
|
mode: z
|
||||||
|
.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc", "inference"])
|
||||||
|
.optional(),
|
||||||
policy: z.string().optional(),
|
policy: z.string().optional(),
|
||||||
ssl: z.boolean().optional(),
|
ssl: z.boolean().optional(),
|
||||||
scheme: z.enum(["http", "https"]).optional(),
|
scheme: z.enum(["http", "https"]).optional(),
|
||||||
@@ -225,7 +282,10 @@ export const PublicResourceSchema = z
|
|||||||
maintenance: MaintenanceSchema.optional(),
|
maintenance: MaintenanceSchema.optional(),
|
||||||
"auth-daemon": AuthDaemonSchema.optional(),
|
"auth-daemon": AuthDaemonSchema.optional(),
|
||||||
"proxy-protocol": z.boolean().optional(),
|
"proxy-protocol": z.boolean().optional(),
|
||||||
"proxy-protocol-version": z.int().min(1).optional()
|
"proxy-protocol-version": z.int().min(1).optional(),
|
||||||
|
labels: z.array(z.string().min(1)).optional(),
|
||||||
|
"ai-providers": z.array(AiProviderAttachmentSchema).optional(),
|
||||||
|
"ai-budget": AiBudgetListSchema
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(resource) => {
|
(resource) => {
|
||||||
@@ -314,11 +374,13 @@ export const PublicResourceSchema = z
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If protocol/mode is http, ssh, rdp, or vnc, it must have a full-domain
|
// If protocol/mode is http, ssh, rdp, vnc, or inference, it must have a full-domain
|
||||||
const effectiveProtocol = resource.mode ?? resource.protocol;
|
const effectiveProtocol = resource.mode ?? resource.protocol;
|
||||||
if (
|
if (
|
||||||
effectiveProtocol !== undefined &&
|
effectiveProtocol !== undefined &&
|
||||||
["http", "ssh", "rdp", "vnc"].includes(effectiveProtocol)
|
["http", "ssh", "rdp", "vnc", "inference"].includes(
|
||||||
|
effectiveProtocol
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
resource["full-domain"] !== undefined &&
|
resource["full-domain"] !== undefined &&
|
||||||
@@ -329,7 +391,43 @@ export const PublicResourceSchema = z
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ["full-domain"],
|
path: ["full-domain"],
|
||||||
error: "When protocol is 'http', 'ssh', 'rdp', or 'vnc', a 'full-domain' must be provided"
|
error: "When protocol is 'http', 'ssh', 'rdp', 'vnc', or 'inference', a 'full-domain' must be provided"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(resource) => {
|
||||||
|
if (isTargetsOnlyResource(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveMode = resource.mode ?? resource.protocol;
|
||||||
|
if (effectiveMode !== "inference") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource.targets.every((target) => target == null);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["targets"],
|
||||||
|
error: "When mode is 'inference', 'targets' must not be provided"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(resource) => {
|
||||||
|
if (isTargetsOnlyResource(resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveMode = resource.mode ?? resource.protocol;
|
||||||
|
if (effectiveMode === "inference") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (resource["ai-providers"]?.length ?? 0) === 0;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["ai-providers"],
|
||||||
|
error: "'ai-providers' can only be set when mode is 'inference'"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -463,7 +561,7 @@ export function isTargetsOnlyResource(resource: any): boolean {
|
|||||||
export const PrivateResourceSchema = z
|
export const PrivateResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
mode: z.enum(["host", "cidr", "http", "ssh"]),
|
mode: z.enum(["host", "cidr", "http", "ssh", "inference"]),
|
||||||
site: z.string().optional(), // DEPRECATED IN FAVOR OF sites
|
site: z.string().optional(), // DEPRECATED IN FAVOR OF sites
|
||||||
sites: z.array(z.string()).optional().default([]),
|
sites: z.array(z.string()).optional().default([]),
|
||||||
// protocol: z.enum(["tcp", "udp"]).optional(),
|
// protocol: z.enum(["tcp", "udp"]).optional(),
|
||||||
@@ -493,16 +591,26 @@ export const PrivateResourceSchema = z
|
|||||||
}),
|
}),
|
||||||
users: z.array(z.string()).optional().default([]),
|
users: z.array(z.string()).optional().default([]),
|
||||||
machines: z.array(z.string()).optional().default([]),
|
machines: z.array(z.string()).optional().default([]),
|
||||||
"auth-daemon": AuthDaemonSchema.optional()
|
labels: z.array(z.string().min(1)).optional().default([]),
|
||||||
|
"auth-daemon": AuthDaemonSchema.optional(),
|
||||||
|
"ai-providers": z
|
||||||
|
.array(AiProviderAttachmentSchema)
|
||||||
|
.optional()
|
||||||
|
.default([]),
|
||||||
|
"ai-budget": AiBudgetListSchemaWithDefault
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// destination is optional only for ssh+native; required for everything else
|
// destination is optional only for ssh+native or inference; required for everything else
|
||||||
const isNativeSSH =
|
const isNativeSSH =
|
||||||
data.mode === "ssh" &&
|
data.mode === "ssh" &&
|
||||||
(data["auth-daemon"] === undefined ||
|
(data["auth-daemon"] === undefined ||
|
||||||
data["auth-daemon"].mode === "native");
|
data["auth-daemon"].mode === "native");
|
||||||
if (!isNativeSSH && !data.destination) {
|
if (
|
||||||
|
data.mode !== "inference" &&
|
||||||
|
!isNativeSSH &&
|
||||||
|
!data.destination
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -510,7 +618,19 @@ export const PrivateResourceSchema = z
|
|||||||
{
|
{
|
||||||
path: ["destination"],
|
path: ["destination"],
|
||||||
message:
|
message:
|
||||||
"destination is required unless mode is 'ssh' with auth-daemon mode 'native'"
|
"destination is required unless mode is 'ssh' with auth-daemon mode 'native', or mode is 'inference'"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.mode === "inference") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (data["ai-providers"]?.length ?? 0) === 0;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["ai-providers"],
|
||||||
|
error: "'ai-providers' can only be set when mode is 'inference'"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -632,7 +752,6 @@ export const ResourcePolicySchema = z.object({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.max(50)
|
|
||||||
.transform((v) => v.map((e) => e.toLowerCase()))
|
.transform((v) => v.map((e) => e.toLowerCase()))
|
||||||
.optional()
|
.optional()
|
||||||
.default([]),
|
.default([]),
|
||||||
@@ -838,3 +957,4 @@ export type Target = z.infer<typeof TargetSchema>;
|
|||||||
export type Resource = z.infer<typeof PublicResourceSchema>;
|
export type Resource = z.infer<typeof PublicResourceSchema>;
|
||||||
export type Config = z.infer<typeof ConfigSchema>;
|
export type Config = z.infer<typeof ConfigSchema>;
|
||||||
export type BlueprintResourcePolicy = z.infer<typeof ResourcePolicySchema>;
|
export type BlueprintResourcePolicy = z.infer<typeof ResourcePolicySchema>;
|
||||||
|
export type BlueprintAiBudget = z.infer<typeof AiBudgetSchema>;
|
||||||
|
|||||||
+15
-15
@@ -10,12 +10,12 @@ export const localCache = new NodeCache({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Log cache statistics periodically for monitoring
|
// Log cache statistics periodically for monitoring
|
||||||
setInterval(() => {
|
// setInterval(() => {
|
||||||
const stats = localCache.getStats();
|
// const stats = localCache.getStats();
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Local cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%`
|
// `Local cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%`
|
||||||
);
|
// );
|
||||||
}, 300000); // Every 5 minutes
|
// }, 300000); // Every 5 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adaptive cache that uses Redis when available in multi-node environments,
|
* Adaptive cache that uses Redis when available in multi-node environments,
|
||||||
@@ -34,9 +34,9 @@ class AdaptiveCache {
|
|||||||
|
|
||||||
// Use local cache as fallback or primary
|
// Use local cache as fallback or primary
|
||||||
const success = localCache.set(key, value, effectiveTtl || 0);
|
const success = localCache.set(key, value, effectiveTtl || 0);
|
||||||
if (success) {
|
// if (success) {
|
||||||
logger.debug(`Set key in local cache: ${key}`);
|
// logger.debug(`Set key in local cache: ${key}`);
|
||||||
}
|
// }
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +48,11 @@ class AdaptiveCache {
|
|||||||
async get<T = any>(key: string): Promise<T | undefined> {
|
async get<T = any>(key: string): Promise<T | undefined> {
|
||||||
// Use local cache as fallback or primary
|
// Use local cache as fallback or primary
|
||||||
const value = localCache.get<T>(key);
|
const value = localCache.get<T>(key);
|
||||||
if (value !== undefined) {
|
// if (value !== undefined) {
|
||||||
logger.debug(`Cache hit in local cache: ${key}`);
|
// logger.debug(`Cache hit in local cache: ${key}`);
|
||||||
} else {
|
// } else {
|
||||||
logger.debug(`Cache miss in local cache: ${key}`);
|
// logger.debug(`Cache miss in local cache: ${key}`);
|
||||||
}
|
// }
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,5 +168,5 @@ class AdaptiveCache {
|
|||||||
|
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
export const cache = new AdaptiveCache();
|
export const cache = new AdaptiveCache();
|
||||||
export const regionalCache = cache; // Alias for compatability with the private version
|
export const regionalCache = cache; // Alias for compatibility with the private version
|
||||||
export default cache;
|
export default cache;
|
||||||
|
|||||||
@@ -339,19 +339,6 @@ export async function calculateUserClientsForOrgs(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get exit nodes for this org
|
|
||||||
const exitNodesList = await getExitNodes(orgId);
|
|
||||||
|
|
||||||
if (exitNodesList.length === 0) {
|
|
||||||
logger.warn(
|
|
||||||
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no exit nodes found`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const randomExitNode =
|
|
||||||
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
|
|
||||||
|
|
||||||
// Get next available subnet
|
// Get next available subnet
|
||||||
const { value: newSubnet, release: releaseSubnetLock } =
|
const { value: newSubnet, release: releaseSubnetLock } =
|
||||||
await getNextAvailableClientSubnet(orgId, trx);
|
await getNextAvailableClientSubnet(orgId, trx);
|
||||||
@@ -370,7 +357,6 @@ export async function calculateUserClientsForOrgs(
|
|||||||
const newClientData: InferInsertModel<typeof clients> = {
|
const newClientData: InferInsertModel<typeof clients> = {
|
||||||
userId,
|
userId,
|
||||||
orgId: userOrg.orgId,
|
orgId: userOrg.orgId,
|
||||||
exitNodeId: randomExitNode.exitNodeId,
|
|
||||||
name: olm.name || "User Client",
|
name: olm.name || "User Client",
|
||||||
subnet: updatedSubnet,
|
subnet: updatedSubnet,
|
||||||
olmId: olm.olmId,
|
olmId: olm.olmId,
|
||||||
|
|||||||
+218
-9
@@ -1,17 +1,226 @@
|
|||||||
export async function getValidCertificatesForDomains(
|
import config from "@server/lib/config";
|
||||||
domains: Set<string>,
|
import { certificates, db } from "@server/db";
|
||||||
useCache: boolean = true
|
import { and, eq, isNotNull, or, inArray, sql } from "drizzle-orm";
|
||||||
): Promise<
|
import { decrypt } from "@server/lib/crypto";
|
||||||
Array<{
|
import logger from "@server/logger";
|
||||||
|
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
|
// Define the return type for clarity and type safety
|
||||||
|
export type CertificateResult = {
|
||||||
id: number;
|
id: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
queriedDomain: string;
|
queriedDomain: string; // The domain that was originally requested (may differ for wildcards)
|
||||||
wildcard: boolean | null;
|
wildcard: boolean | null;
|
||||||
certFile: string | null;
|
certFile: string | null;
|
||||||
keyFile: string | null;
|
keyFile: string | null;
|
||||||
expiresAt: number | null;
|
expiresAt: number | null;
|
||||||
updatedAt?: number | null;
|
updatedAt?: number | null;
|
||||||
}>
|
};
|
||||||
> {
|
|
||||||
return []; // stub
|
export async function getValidCertificatesForDomains(
|
||||||
|
domains: Set<string>,
|
||||||
|
useCache: boolean = true
|
||||||
|
): Promise<Array<CertificateResult>> {
|
||||||
|
const finalResults: CertificateResult[] = [];
|
||||||
|
const domainsToQuery = new Set<string>();
|
||||||
|
|
||||||
|
// 1. Check cache first if enabled
|
||||||
|
if (useCache) {
|
||||||
|
for (const domain of domains) {
|
||||||
|
const cacheKey = `cert:${domain}`;
|
||||||
|
const cachedCert = await cache.get<CertificateResult>(cacheKey);
|
||||||
|
if (cachedCert) {
|
||||||
|
finalResults.push(cachedCert); // Valid cache hit
|
||||||
|
} else {
|
||||||
|
// Also check for a wildcard cache entry covering this domain's parent
|
||||||
|
const parts = domain.split(".");
|
||||||
|
let wildcardHit = false;
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const parentDomain = parts.slice(1).join(".");
|
||||||
|
const wildcardCacheKey = `cert:*.${parentDomain}`;
|
||||||
|
const cachedWildcard =
|
||||||
|
await cache.get<CertificateResult>(wildcardCacheKey);
|
||||||
|
if (cachedWildcard) {
|
||||||
|
// Re-stamp queriedDomain so callers see the originally requested domain
|
||||||
|
finalResults.push({
|
||||||
|
...cachedWildcard,
|
||||||
|
queriedDomain: domain
|
||||||
|
});
|
||||||
|
wildcardHit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!wildcardHit) {
|
||||||
|
domainsToQuery.add(domain); // Cache miss or expired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If caching is disabled, add all domains to the query set
|
||||||
|
domains.forEach((d) => domainsToQuery.add(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If all domains were resolved from the cache, return early
|
||||||
|
if (domainsToQuery.size === 0) {
|
||||||
|
const decryptedResults = decryptFinalResults(
|
||||||
|
finalResults,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
return decryptedResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Prepare domains for the database query
|
||||||
|
const domainsToQueryArray = Array.from(domainsToQuery);
|
||||||
|
const parentDomainsToQuery = new Set<string>();
|
||||||
|
|
||||||
|
domainsToQueryArray.forEach((domain) => {
|
||||||
|
const parts = domain.split(".");
|
||||||
|
// A wildcard can only match a domain with at least two parts (e.g., example.com)
|
||||||
|
if (parts.length > 1) {
|
||||||
|
parentDomainsToQuery.add(parts.slice(1).join("."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentDomainsArray = Array.from(parentDomainsToQuery);
|
||||||
|
|
||||||
|
// Build wildcard variants: for each parent domain "example.com", also query "*.example.com"
|
||||||
|
const wildcardPrefixedArray =
|
||||||
|
build != "saas" ? parentDomainsArray.map((d) => `*.${d}`) : [];
|
||||||
|
|
||||||
|
// 4. Build and execute a single, efficient Drizzle query
|
||||||
|
// This query fetches all potential exact and wildcard matches in one database round-trip.
|
||||||
|
const potentialCerts = await db
|
||||||
|
.select()
|
||||||
|
.from(certificates)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(certificates.status, "valid"),
|
||||||
|
isNotNull(certificates.certFile),
|
||||||
|
isNotNull(certificates.keyFile),
|
||||||
|
or(
|
||||||
|
// Condition for exact matches on the requested domains
|
||||||
|
inArray(certificates.domain, domainsToQueryArray),
|
||||||
|
// Condition for wildcard matches on the parent domains (stored as "example.com" or "*.example.com")
|
||||||
|
parentDomainsArray.length > 0
|
||||||
|
? and(
|
||||||
|
inArray(certificates.domain, [
|
||||||
|
...parentDomainsArray,
|
||||||
|
...wildcardPrefixedArray
|
||||||
|
]),
|
||||||
|
eq(certificates.wildcard, true)
|
||||||
|
)
|
||||||
|
: // If there are no possible parent domains, this condition is false
|
||||||
|
sql`false`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Helper to normalize a wildcard cert's domain to its bare parent domain (strips leading "*.")
|
||||||
|
const normalizeWildcardDomain = (domain: string): string =>
|
||||||
|
domain.startsWith("*.") ? domain.slice(2) : domain;
|
||||||
|
|
||||||
|
// 5. Process the database results, prioritizing exact matches over wildcards
|
||||||
|
const exactMatches = new Map<string, (typeof potentialCerts)[0]>();
|
||||||
|
const wildcardMatches = new Map<string, (typeof potentialCerts)[0]>();
|
||||||
|
|
||||||
|
for (const cert of potentialCerts) {
|
||||||
|
if (cert.wildcard) {
|
||||||
|
// Normalize to bare parent domain so lookups are consistent regardless of storage format
|
||||||
|
wildcardMatches.set(normalizeWildcardDomain(cert.domain), cert);
|
||||||
|
} else {
|
||||||
|
exactMatches.set(cert.domain, cert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const domain of domainsToQuery) {
|
||||||
|
let foundCert: (typeof potentialCerts)[0] | undefined = undefined;
|
||||||
|
|
||||||
|
// Priority 1: Check for an exact match (non-wildcard)
|
||||||
|
if (exactMatches.has(domain)) {
|
||||||
|
foundCert = exactMatches.get(domain);
|
||||||
|
}
|
||||||
|
// Priority 2: Check for a wildcard certificate whose normalized domain equals the queried domain
|
||||||
|
else {
|
||||||
|
const normalizedDomain = normalizeWildcardDomain(domain);
|
||||||
|
if (wildcardMatches.has(normalizedDomain)) {
|
||||||
|
foundCert = wildcardMatches.get(normalizedDomain);
|
||||||
|
}
|
||||||
|
// Priority 3: Check for a wildcard match on the parent domain
|
||||||
|
else {
|
||||||
|
const parts = normalizedDomain.split(".");
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const parentDomain = parts.slice(1).join(".");
|
||||||
|
if (wildcardMatches.has(parentDomain)) {
|
||||||
|
foundCert = wildcardMatches.get(parentDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a certificate was found, format it, add to results, and cache it
|
||||||
|
if (foundCert) {
|
||||||
|
logger.debug(
|
||||||
|
`Creating result cert for ${domain} using cert from ${foundCert.domain}`
|
||||||
|
);
|
||||||
|
const resultCert: CertificateResult = {
|
||||||
|
id: foundCert.certId,
|
||||||
|
domain: foundCert.domain, // The actual domain of the cert record
|
||||||
|
queriedDomain: domain, // The domain that was originally requested
|
||||||
|
wildcard: foundCert.wildcard,
|
||||||
|
certFile: foundCert.certFile,
|
||||||
|
keyFile: foundCert.keyFile,
|
||||||
|
expiresAt: foundCert.expiresAt,
|
||||||
|
updatedAt: foundCert.updatedAt
|
||||||
|
};
|
||||||
|
|
||||||
|
finalResults.push(resultCert);
|
||||||
|
|
||||||
|
// Add to cache for future requests, using the *requested domain* as the key
|
||||||
|
if (useCache) {
|
||||||
|
const cacheKey = `cert:${domain}`;
|
||||||
|
await cache.set(cacheKey, resultCert, 180);
|
||||||
|
|
||||||
|
// Also cache wildcard certs under a pattern key so other subdomains
|
||||||
|
// can find them without a DB round-trip
|
||||||
|
if (resultCert.wildcard) {
|
||||||
|
const normalizedCertDomain = normalizeWildcardDomain(
|
||||||
|
resultCert.domain
|
||||||
|
);
|
||||||
|
const wildcardCacheKey = `cert:*.${normalizedCertDomain}`;
|
||||||
|
await cache.set(wildcardCacheKey, resultCert, 180);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedResults = decryptFinalResults(
|
||||||
|
finalResults,
|
||||||
|
config.getRawConfig().server.secret!
|
||||||
|
);
|
||||||
|
return decryptedResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decryptFinalResults(
|
||||||
|
finalResults: CertificateResult[],
|
||||||
|
secret: string
|
||||||
|
): CertificateResult[] {
|
||||||
|
const validCertsDecrypted = finalResults.map((cert) => {
|
||||||
|
// Decrypt and save certificate file
|
||||||
|
const decryptedCert = decrypt(
|
||||||
|
cert.certFile!, // is not null from query
|
||||||
|
secret
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decrypt and save key file
|
||||||
|
const decryptedKey = decrypt(cert.keyFile!, secret);
|
||||||
|
|
||||||
|
// Return only the certificate data without org information
|
||||||
|
return {
|
||||||
|
...cert,
|
||||||
|
certFile: decryptedCert,
|
||||||
|
keyFile: decryptedKey
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return validCertsDecrypted;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { cleanUpOldLogs as cleanUpOldAccessLogs } from "#dynamic/lib/logAccessAu
|
|||||||
import { cleanUpOldLogs as cleanUpOldActionLogs } from "#dynamic/middlewares/logActionAudit";
|
import { cleanUpOldLogs as cleanUpOldActionLogs } from "#dynamic/middlewares/logActionAudit";
|
||||||
import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit";
|
import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit";
|
||||||
import { cleanUpOldLogs as cleanUpOldConnectionLogs } from "#dynamic/routers/newt";
|
import { cleanUpOldLogs as cleanUpOldConnectionLogs } from "#dynamic/routers/newt";
|
||||||
|
import { cleanUpOldLogs as cleanUpOldAiSessionLogs } from "@server/routers/aiGateway/logAiSession";
|
||||||
import { gt, or } from "drizzle-orm";
|
import { gt, or } from "drizzle-orm";
|
||||||
import { cleanUpOldFingerprintSnapshots } from "@server/routers/olm/fingerprintingUtils";
|
import { cleanUpOldFingerprintSnapshots } from "@server/routers/olm/fingerprintingUtils";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export function initLogCleanupInterval() {
|
export function initLogCleanupInterval() {
|
||||||
if (build == "saas") { // skip log cleanup for saas builds
|
if (build == "saas") {
|
||||||
|
// skip log cleanup for saas builds
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return setInterval(
|
return setInterval(
|
||||||
@@ -23,7 +25,9 @@ export function initLogCleanupInterval() {
|
|||||||
settingsLogRetentionDaysRequest:
|
settingsLogRetentionDaysRequest:
|
||||||
orgs.settingsLogRetentionDaysRequest,
|
orgs.settingsLogRetentionDaysRequest,
|
||||||
settingsLogRetentionDaysConnection:
|
settingsLogRetentionDaysConnection:
|
||||||
orgs.settingsLogRetentionDaysConnection
|
orgs.settingsLogRetentionDaysConnection,
|
||||||
|
settingsLogRetentionDaysAISessions:
|
||||||
|
orgs.settingsLogRetentionDaysAISessions
|
||||||
})
|
})
|
||||||
.from(orgs)
|
.from(orgs)
|
||||||
.where(
|
.where(
|
||||||
@@ -31,7 +35,8 @@ export function initLogCleanupInterval() {
|
|||||||
gt(orgs.settingsLogRetentionDaysAction, 0),
|
gt(orgs.settingsLogRetentionDaysAction, 0),
|
||||||
gt(orgs.settingsLogRetentionDaysAccess, 0),
|
gt(orgs.settingsLogRetentionDaysAccess, 0),
|
||||||
gt(orgs.settingsLogRetentionDaysRequest, 0),
|
gt(orgs.settingsLogRetentionDaysRequest, 0),
|
||||||
gt(orgs.settingsLogRetentionDaysConnection, 0)
|
gt(orgs.settingsLogRetentionDaysConnection, 0),
|
||||||
|
gt(orgs.settingsLogRetentionDaysAISessions, 0)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -42,7 +47,8 @@ export function initLogCleanupInterval() {
|
|||||||
settingsLogRetentionDaysAction,
|
settingsLogRetentionDaysAction,
|
||||||
settingsLogRetentionDaysAccess,
|
settingsLogRetentionDaysAccess,
|
||||||
settingsLogRetentionDaysRequest,
|
settingsLogRetentionDaysRequest,
|
||||||
settingsLogRetentionDaysConnection
|
settingsLogRetentionDaysConnection,
|
||||||
|
settingsLogRetentionDaysAISessions
|
||||||
} = org;
|
} = org;
|
||||||
|
|
||||||
if (settingsLogRetentionDaysAction > 0) {
|
if (settingsLogRetentionDaysAction > 0) {
|
||||||
@@ -72,6 +78,13 @@ export function initLogCleanupInterval() {
|
|||||||
settingsLogRetentionDaysConnection
|
settingsLogRetentionDaysConnection
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingsLogRetentionDaysAISessions > 0) {
|
||||||
|
await cleanUpOldAiSessionLogs(
|
||||||
|
orgId,
|
||||||
|
settingsLogRetentionDaysAISessions
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await cleanUpOldFingerprintSnapshots(365);
|
await cleanUpOldFingerprintSnapshots(365);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.21.1";
|
export const APP_VERSION = "1.22.0";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ export async function deleteOrgById(
|
|||||||
await trx.delete(sites).where(eq(sites.siteId, site.siteId));
|
await trx.delete(sites).where(eq(sites.siteId, site.siteId));
|
||||||
}
|
}
|
||||||
for (const client of orgClients) {
|
for (const client of orgClients) {
|
||||||
|
if (client.exitNodeId && client.pubKey) {
|
||||||
|
await deletePeer(client.exitNodeId, client.pubKey);
|
||||||
|
}
|
||||||
const [olm] = await trx
|
const [olm] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(olms)
|
.from(olms)
|
||||||
|
|||||||
@@ -64,13 +64,20 @@ export async function performDeleteResources(
|
|||||||
|
|
||||||
const targetsByResourceId = new Map<number, Target[]>();
|
const targetsByResourceId = new Map<number, Target[]>();
|
||||||
for (const target of targetsToBeRemoved) {
|
for (const target of targetsToBeRemoved) {
|
||||||
|
if (target.resourceId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const existing = targetsByResourceId.get(target.resourceId) ?? [];
|
const existing = targetsByResourceId.get(target.resourceId) ?? [];
|
||||||
existing.push(target);
|
existing.push(target);
|
||||||
targetsByResourceId.set(target.resourceId, existing);
|
targetsByResourceId.set(target.resourceId, existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetIdToResourceId = new Map(
|
const targetIdToResourceId = new Map(
|
||||||
targetsToBeRemoved.map((target) => [target.targetId, target.resourceId])
|
targetsToBeRemoved.flatMap((target) =>
|
||||||
|
target.resourceId == null
|
||||||
|
? []
|
||||||
|
: [[target.targetId, target.resourceId] as const]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const healthChecksByResourceId = new Map<number, TargetHealthCheck[]>();
|
const healthChecksByResourceId = new Map<number, TargetHealthCheck[]>();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
import { and, eq, inArray, isNotNull, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
resources,
|
resources,
|
||||||
@@ -33,9 +33,11 @@ export async function getResourceIdsForSite(
|
|||||||
const rows = await trx
|
const rows = await trx
|
||||||
.selectDistinct({ resourceId: targets.resourceId })
|
.selectDistinct({ resourceId: targets.resourceId })
|
||||||
.from(targets)
|
.from(targets)
|
||||||
.where(eq(targets.siteId, siteId));
|
.where(and(eq(targets.siteId, siteId), isNotNull(targets.resourceId)));
|
||||||
|
|
||||||
return rows.map((row) => row.resourceId);
|
return rows
|
||||||
|
.map((row) => row.resourceId)
|
||||||
|
.filter((resourceId): resourceId is number => resourceId != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSiteResourceIdsForSite(
|
export async function getSiteResourceIdsForSite(
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { db, sites, clients } from "@server/db";
|
||||||
|
import { and, eq, count } from "drizzle-orm";
|
||||||
|
|
||||||
|
// (MAX_CONNECTIONS - current_connections) / MAX_CONNECTIONS)
|
||||||
|
// higher = more desirable
|
||||||
|
// like saying, this node has x% of its capacity left
|
||||||
|
export async function calculateExitNodeWeight(
|
||||||
|
exitNodeId: number,
|
||||||
|
maxConnections: number | null | undefined
|
||||||
|
): Promise<number | null> {
|
||||||
|
if (maxConnections === null || maxConnections === undefined) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [[siteConnections], [clientConnections]] = await Promise.all([
|
||||||
|
db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(sites)
|
||||||
|
.where(
|
||||||
|
and(eq(sites.exitNodeId, exitNodeId), eq(sites.online, true))
|
||||||
|
),
|
||||||
|
db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clients.exitNodeId, exitNodeId),
|
||||||
|
eq(clients.online, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currentConnections = siteConnections.count + clientConnections.count;
|
||||||
|
|
||||||
|
if (currentConnections >= maxConnections) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (maxConnections - currentConnections) / maxConnections;
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { db, exitNodes, Transaction } from "@server/db";
|
import { db, exitNodes, Transaction } from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { ExitNodePingResult } from "@server/routers/newt";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export async function verifyExitNodeOrgAccess(
|
export async function verifyExitNodeOrgAccess(
|
||||||
@@ -52,6 +51,16 @@ export async function listExitNodes(
|
|||||||
return allExitNodes;
|
return allExitNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ExitNodePingResult = {
|
||||||
|
exitNodeId: number;
|
||||||
|
latencyMs: number;
|
||||||
|
weight: number;
|
||||||
|
error?: string;
|
||||||
|
exitNodeName: string;
|
||||||
|
endpoint: string;
|
||||||
|
wasPreviouslyConnected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function selectBestExitNode(
|
export function selectBestExitNode(
|
||||||
pingResults: ExitNodePingResult[]
|
pingResults: ExitNodePingResult[]
|
||||||
): ExitNodePingResult | null {
|
): ExitNodePingResult | null {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { db, ExitNode, Transaction, sites, clients } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import { findNextAvailableCidr } from "@server/lib/ip";
|
||||||
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
|
|
||||||
|
export async function getUniqueSubnetForExitNode(
|
||||||
|
exitNode: ExitNode,
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<string | null> {
|
||||||
|
const lockKey = `subnet-allocation:${exitNode.exitNodeId}`;
|
||||||
|
|
||||||
|
return await lockManager.withLock(
|
||||||
|
lockKey,
|
||||||
|
async () => {
|
||||||
|
const [sitesQuery, clientsQuery] = await Promise.all([
|
||||||
|
trx
|
||||||
|
.select({ subnet: sites.exitNodeSubnet })
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.exitNodeId, exitNode.exitNodeId)),
|
||||||
|
trx
|
||||||
|
.select({ subnet: clients.exitNodeSubnet })
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.exitNodeId, exitNode.exitNodeId))
|
||||||
|
]);
|
||||||
|
|
||||||
|
const blockSize = config.getRawConfig().gerbil.site_block_size;
|
||||||
|
const subnets = [...sitesQuery, ...clientsQuery]
|
||||||
|
.map((row) => row.subnet)
|
||||||
|
.filter(
|
||||||
|
(subnet): subnet is string =>
|
||||||
|
!!subnet &&
|
||||||
|
/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet)
|
||||||
|
);
|
||||||
|
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
|
||||||
|
|
||||||
|
return findNextAvailableCidr(subnets, blockSize, exitNode.address);
|
||||||
|
},
|
||||||
|
5000 // 5 second lock TTL - subnet allocation should be quick
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,3 +2,5 @@ export * from "./exitNodes";
|
|||||||
export * from "./exitNodeComms";
|
export * from "./exitNodeComms";
|
||||||
export * from "./subnet";
|
export * from "./subnet";
|
||||||
export * from "./getCurrentExitNodeId";
|
export * from "./getCurrentExitNodeId";
|
||||||
|
export * from "./calculateExitNodeWeight";
|
||||||
|
export * from "./getUniqueSubnetForExitNode";
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { db, idp, idpOrg, Transaction } from "@server/db";
|
import { db, idp, idpOrg, Transaction } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export function isOrgIdentityProviderMode(): boolean {
|
export function isOrgIdentityProviderMode(): boolean {
|
||||||
return process.env.IDENTITY_PROVIDER_MODE === "org";
|
return build === "saas" || process.env.IDENTITY_PROVIDER_MODE === "org";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+22
-9
@@ -6,7 +6,7 @@ import z from "zod";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
import { getValidCertificatesForDomains } from "@server/lib/certificates";
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
|
|
||||||
interface IPRange {
|
interface IPRange {
|
||||||
@@ -528,7 +528,10 @@ export function generateRemoteSubnets(
|
|||||||
|
|
||||||
export type Alias = { alias: string | null; aliasAddress: string | null };
|
export type Alias = { alias: string | null; aliasAddress: string | null };
|
||||||
|
|
||||||
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
export function generateAliasConfig(
|
||||||
|
allSiteResources: SiteResource[],
|
||||||
|
overrideIp?: string
|
||||||
|
): Alias[] {
|
||||||
return allSiteResources
|
return allSiteResources
|
||||||
.filter(
|
.filter(
|
||||||
(sr) =>
|
(sr) =>
|
||||||
@@ -539,7 +542,7 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
|||||||
)
|
)
|
||||||
.map((sr) => ({
|
.map((sr) => ({
|
||||||
alias: sr.alias || sr.fullDomain,
|
alias: sr.alias || sr.fullDomain,
|
||||||
aliasAddress: sr.aliasAddress
|
aliasAddress: overrideIp || sr.aliasAddress
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,9 +663,10 @@ export type CertRef = { id: string; cert: string; key: string };
|
|||||||
* certificate (e.g. a wildcard cert used by thousands of site resources)
|
* certificate (e.g. a wildcard cert used by thousands of site resources)
|
||||||
* only need that certificate sent once per sync message.
|
* only need that certificate sent once per sync message.
|
||||||
*/
|
*/
|
||||||
export function dedupeCertsForTargets(
|
export function dedupeCertsForTargets(targetsV2: SubnetProxyTargetV2[]): {
|
||||||
targetsV2: SubnetProxyTargetV2[]
|
targets: SubnetProxyTargetV2[];
|
||||||
): { targets: SubnetProxyTargetV2[]; certs: CertRef[] } {
|
certs: CertRef[];
|
||||||
|
} {
|
||||||
const idByContent = new Map<string, string>();
|
const idByContent = new Map<string, string>();
|
||||||
const certs: CertRef[] = [];
|
const certs: CertRef[] = [];
|
||||||
|
|
||||||
@@ -674,7 +678,10 @@ export function dedupeCertsForTargets(
|
|||||||
const contentKey = `${target.tlsCert}|${target.tlsKey}`;
|
const contentKey = `${target.tlsCert}|${target.tlsKey}`;
|
||||||
let id = idByContent.get(contentKey);
|
let id = idByContent.get(contentKey);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
id = createHash("sha1").update(contentKey).digest("hex").slice(0, 16);
|
id = createHash("sha1")
|
||||||
|
.update(contentKey)
|
||||||
|
.digest("hex")
|
||||||
|
.slice(0, 16);
|
||||||
idByContent.set(contentKey, id);
|
idByContent.set(contentKey, id);
|
||||||
certs.push({ id, cert: target.tlsCert, key: target.tlsKey });
|
certs.push({ id, cert: target.tlsCert, key: target.tlsKey });
|
||||||
}
|
}
|
||||||
@@ -708,7 +715,9 @@ export async function batchFetchCertsForSiteResources(
|
|||||||
): Promise<CertByDomain> {
|
): Promise<CertByDomain> {
|
||||||
const domains = new Set(
|
const domains = new Set(
|
||||||
allSiteResources
|
allSiteResources
|
||||||
.filter((r) => r.enabled && r.mode === "http" && r.ssl && r.fullDomain)
|
.filter(
|
||||||
|
(r) => r.enabled && r.mode === "http" && r.ssl && r.fullDomain
|
||||||
|
)
|
||||||
.map((r) => r.fullDomain as string)
|
.map((r) => r.fullDomain as string)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -852,7 +861,11 @@ export async function generateSubnetProxyTargetV2(
|
|||||||
new Set([siteResource.fullDomain]),
|
new Set([siteResource.fullDomain]),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
if (certs.length > 0 && certs[0].certFile && certs[0].keyFile) {
|
if (
|
||||||
|
certs.length > 0 &&
|
||||||
|
certs[0].certFile &&
|
||||||
|
certs[0].keyFile
|
||||||
|
) {
|
||||||
tlsCert = certs[0].certFile;
|
tlsCert = certs[0].certFile;
|
||||||
tlsKey = certs[0].keyFile;
|
tlsKey = certs[0].keyFile;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export async function logAccessAudit(data: {
|
|||||||
type: string;
|
type: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
resourceId?: number;
|
resourceId?: number;
|
||||||
|
siteResourceId?: number;
|
||||||
user?: { username: string; userId: string };
|
user?: { username: string; userId: string };
|
||||||
apiKey?: { name: string | null; apiKeyId: string };
|
apiKey?: { name: string | null; apiKeyId: string };
|
||||||
metadata?: any;
|
metadata?: any;
|
||||||
|
|||||||
@@ -79,7 +79,13 @@ export const configSchema = z
|
|||||||
.default(3001)
|
.default(3001)
|
||||||
.transform(stoi)
|
.transform(stoi)
|
||||||
.pipe(portSchema),
|
.pipe(portSchema),
|
||||||
|
ai_gateway_port: portSchema
|
||||||
|
.optional()
|
||||||
|
.default(3005)
|
||||||
|
.transform(stoi)
|
||||||
|
.pipe(portSchema),
|
||||||
badger_override: z.string().optional(),
|
badger_override: z.string().optional(),
|
||||||
|
ai_gateway_override: z.string().optional(),
|
||||||
next_port: portSchema
|
next_port: portSchema
|
||||||
.optional()
|
.optional()
|
||||||
.default(3002)
|
.default(3002)
|
||||||
@@ -105,6 +111,23 @@ export const configSchema = z
|
|||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
|
remote_headers: z
|
||||||
|
.object({
|
||||||
|
user_id: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("Remote-User-Id"),
|
||||||
|
virtual_api_key_id: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("Remote-Virtual-Api-Key-Id"),
|
||||||
|
user: z.string().optional().default("Remote-User"),
|
||||||
|
email: z.string().optional().default("Remote-Email"),
|
||||||
|
name: z.string().optional().default("Remote-Name"),
|
||||||
|
role: z.string().optional().default("Remote-Role")
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.prefault({}),
|
||||||
resource_session_request_param: z
|
resource_session_request_param: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -130,6 +153,24 @@ export const configSchema = z
|
|||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
trust_proxy: z.int().gte(0).optional().default(1),
|
trust_proxy: z.int().gte(0).optional().default(1),
|
||||||
|
// Opt-in: have Traefik/Badger stamp the resolved client IP
|
||||||
|
// into a dedicated header (X-Pangolin-Client-Ip) on the
|
||||||
|
// site-resource AI gateway route, so it survives an
|
||||||
|
// intermediary proxy between Traefik and the gateway that
|
||||||
|
// overwrites X-Forwarded-For/X-Real-Ip instead of appending
|
||||||
|
// to them. Off by default since it requires a Badger
|
||||||
|
// version that supports realIpHeader.
|
||||||
|
enable_ai_gateway_client_ip_header: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(false)
|
||||||
|
.transform((val) =>
|
||||||
|
process.env.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER !==
|
||||||
|
undefined
|
||||||
|
? process.env.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER ===
|
||||||
|
"true"
|
||||||
|
: val
|
||||||
|
),
|
||||||
secret: z.string().pipe(z.string().min(8)).optional(),
|
secret: z.string().pipe(z.string().min(8)).optional(),
|
||||||
maxmind_db_path: z.string().optional(),
|
maxmind_db_path: z.string().optional(),
|
||||||
maxmind_asn_path: z.string().optional()
|
maxmind_asn_path: z.string().optional()
|
||||||
@@ -139,6 +180,7 @@ export const configSchema = z
|
|||||||
integration_port: 3003,
|
integration_port: 3003,
|
||||||
external_port: 3000,
|
external_port: 3000,
|
||||||
internal_port: 3001,
|
internal_port: 3001,
|
||||||
|
ai_gateway_port: 3005,
|
||||||
next_port: 3002,
|
next_port: 3002,
|
||||||
internal_hostname: "pangolin",
|
internal_hostname: "pangolin",
|
||||||
session_cookie_name: "p_session_token",
|
session_cookie_name: "p_session_token",
|
||||||
@@ -147,11 +189,20 @@ export const configSchema = z
|
|||||||
id: "P-Access-Token-Id",
|
id: "P-Access-Token-Id",
|
||||||
token: "P-Access-Token"
|
token: "P-Access-Token"
|
||||||
},
|
},
|
||||||
|
remote_headers: {
|
||||||
|
user_id: "Remote-User-Id",
|
||||||
|
virtual_api_key_id: "Remote-Virtual-Api-Key-Id",
|
||||||
|
user: "Remote-User",
|
||||||
|
email: "Remote-Email",
|
||||||
|
name: "Remote-Name",
|
||||||
|
role: "Remote-Role"
|
||||||
|
},
|
||||||
resource_session_request_param:
|
resource_session_request_param:
|
||||||
"resource_session_request_param",
|
"resource_session_request_param",
|
||||||
dashboard_session_length_hours: 720,
|
dashboard_session_length_hours: 720,
|
||||||
resource_session_length_hours: 720,
|
resource_session_length_hours: 720,
|
||||||
trust_proxy: 1
|
trust_proxy: 1,
|
||||||
|
enable_ai_gateway_client_ip_header: false
|
||||||
}),
|
}),
|
||||||
postgres: z
|
postgres: z
|
||||||
.object({
|
.object({
|
||||||
@@ -391,9 +442,54 @@ export const configSchema = z
|
|||||||
disable_basic_wireguard_sites: z.boolean().optional(),
|
disable_basic_wireguard_sites: z.boolean().optional(),
|
||||||
disable_config_managed_domains: z.boolean().optional(),
|
disable_config_managed_domains: z.boolean().optional(),
|
||||||
disable_product_help_banners: z.boolean().optional(),
|
disable_product_help_banners: z.boolean().optional(),
|
||||||
disable_enterprise_features: z.boolean().optional()
|
disable_enterprise_features: z.boolean().optional(),
|
||||||
|
enable_acme_cert_sync: z.boolean().optional().default(true)
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
acme: z
|
||||||
|
.object({
|
||||||
|
acme_json_path: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("config/letsencrypt/acme.json"),
|
||||||
|
acme_http_endpoint: z.string().optional(),
|
||||||
|
sync_interval_ms: z.number().optional().default(5000)
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
ai: z
|
||||||
|
.object({
|
||||||
|
model_catalog: z
|
||||||
|
.object({
|
||||||
|
upstream_url: z
|
||||||
|
.url()
|
||||||
|
.optional()
|
||||||
|
.default("https://api.fossorial.io/api/v1/models"),
|
||||||
|
// No default - only used when an operator wants to
|
||||||
|
// pin the catalog to a local file instead of
|
||||||
|
// fetching it from upstream_url.
|
||||||
|
file: z.string().optional(),
|
||||||
|
// No default - only used when an operator wants to
|
||||||
|
// merge the content of the json file with the upstream catalog. This is useful for adding
|
||||||
|
// custom models to the catalog without having to maintain a separate fork of the upstream catalog.
|
||||||
|
merge_file: z.string().optional(),
|
||||||
|
refresh_interval_min_hours: z
|
||||||
|
.number()
|
||||||
|
.positive()
|
||||||
|
.gt(0)
|
||||||
|
.optional()
|
||||||
|
.default(6),
|
||||||
|
refresh_interval_max_hours: z
|
||||||
|
.number()
|
||||||
|
.positive()
|
||||||
|
.gt(0)
|
||||||
|
.optional()
|
||||||
|
.default(12)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.prefault({})
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.prefault({}),
|
||||||
dns: z
|
dns: z
|
||||||
.object({
|
.object({
|
||||||
nameservers: z
|
nameservers: z
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
userOrgRoles,
|
userOrgRoles,
|
||||||
userSiteResources
|
userSiteResources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, count, eq, inArray, ne } from "drizzle-orm";
|
import { and, count, eq, inArray, isNotNull, ne } from "drizzle-orm";
|
||||||
|
|
||||||
import { deletePeersBatch as newtDeletePeersBatch } from "@server/routers/newt/peers";
|
import { deletePeersBatch as newtDeletePeersBatch } from "@server/routers/newt/peers";
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +27,9 @@ import {
|
|||||||
deletePeersBatch as olmDeletePeersBatch
|
deletePeersBatch as olmDeletePeersBatch
|
||||||
} from "@server/routers/olm/peers";
|
} from "@server/routers/olm/peers";
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
|
import { sendToClientsBatch } from "#dynamic/routers/ws";
|
||||||
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
|
import config from "@server/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
generateAliasConfig,
|
generateAliasConfig,
|
||||||
@@ -187,7 +190,12 @@ export async function getClientSiteResourceAccess(
|
|||||||
`rebuildClientAssociations: [getClientSiteResourceAccess] siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} siteCount=${sitesList.length} siteIds=[${sitesList.map((s) => s.siteId).join(", ")}]`
|
`rebuildClientAssociations: [getClientSiteResourceAccess] siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} siteCount=${sitesList.length} siteIds=[${sitesList.map((s) => s.siteId).join(", ")}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sitesList.length === 0) {
|
if (sitesList.length === 0 && siteResource.networkId !== null) {
|
||||||
|
// A site resource with a networkId is expected to have at least one
|
||||||
|
// site attached via siteNetworks. Resources with no networkId (e.g.
|
||||||
|
// inference-mode resources, which connect clients directly to the
|
||||||
|
// exit node instead of any site) are expected to have no sites, so
|
||||||
|
// don't warn for those.
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`No sites found for siteResource ${siteResource.siteResourceId} with networkId ${siteResource.networkId}`
|
`No sites found for siteResource ${siteResource.siteResourceId} with networkId ${siteResource.networkId}`
|
||||||
);
|
);
|
||||||
@@ -687,6 +695,22 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
|
|||||||
clientSiteResourcesToRemove,
|
clientSiteResourcesToRemove,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If this resource requires clients to be connected to the exit node
|
||||||
|
// (e.g. an inference resource), re-sync the connect/disconnect state for
|
||||||
|
// every client whose access to it may have changed - both those who
|
||||||
|
// currently have access and those who just lost it.
|
||||||
|
if (siteResource.requiresExitNodeConnection) {
|
||||||
|
await syncClientExitNodeConnections(
|
||||||
|
Array.from(
|
||||||
|
new Set([
|
||||||
|
...mergedAllClientIds,
|
||||||
|
...existingClientSiteResourceIds
|
||||||
|
])
|
||||||
|
),
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMessagesForSiteClients(
|
async function handleMessagesForSiteClients(
|
||||||
@@ -966,7 +990,7 @@ export async function updateClientSiteDestinations(
|
|||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||||
|
|
||||||
for (const site of sitesData) {
|
for (const site of sitesData) {
|
||||||
if (!site.sites.subnet) {
|
if (!site.sites.exitNodeSubnet) {
|
||||||
logger.debug(`Site ${site.sites.siteId} has no subnet, skipping`);
|
logger.debug(`Site ${site.sites.siteId} has no subnet, skipping`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1002,7 +1026,7 @@ export async function updateClientSiteDestinations(
|
|||||||
sourcePort: parsedEndpoint.port,
|
sourcePort: parsedEndpoint.port,
|
||||||
destinations: [
|
destinations: [
|
||||||
{
|
{
|
||||||
destinationIP: site.sites.subnet.split("/")[0],
|
destinationIP: site.sites.exitNodeSubnet.split("/")[0],
|
||||||
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1010,7 +1034,7 @@ export async function updateClientSiteDestinations(
|
|||||||
} else {
|
} else {
|
||||||
// add to the existing destinations
|
// add to the existing destinations
|
||||||
destinations.destinations.push({
|
destinations.destinations.push({
|
||||||
destinationIP: site.sites.subnet.split("/")[0],
|
destinationIP: site.sites.exitNodeSubnet.split("/")[0],
|
||||||
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1052,6 +1076,265 @@ export async function updateClientSiteDestinations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determines, for each of the given clients, whether they currently have
|
||||||
|
// access to any enabled site resource with requiresExitNodeConnection set
|
||||||
|
// (e.g. an inference-mode resource) and tells the client's olm to connect to
|
||||||
|
// or disconnect from its assigned exit node accordingly. Site resources with
|
||||||
|
// requiresExitNodeConnection don't belong to any site/network, so this can't
|
||||||
|
// be derived from the per-site peer logic above - it has to be recomputed
|
||||||
|
// from the client's full current resource access every time that access
|
||||||
|
// changes.
|
||||||
|
async function syncClientExitNodeConnections(
|
||||||
|
clientIds: number[],
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<void> {
|
||||||
|
const uniqueClientIds = Array.from(new Set(clientIds));
|
||||||
|
if (uniqueClientIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only clients with an exit node assigned can be told to connect/disconnect.
|
||||||
|
const clientsData = await trx
|
||||||
|
.select({
|
||||||
|
clientId: clients.clientId,
|
||||||
|
exitNodeId: clients.exitNodeId,
|
||||||
|
exitNodeSubnet: clients.exitNodeSubnet
|
||||||
|
})
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(clients.clientId, uniqueClientIds),
|
||||||
|
isNotNull(clients.exitNodeId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clientsData.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientIdsWithExitNode = clientsData.map((c) => c.clientId);
|
||||||
|
|
||||||
|
const requiresExitNodeRows = await trx
|
||||||
|
.select({
|
||||||
|
clientId: clientSiteResourcesAssociationsCache.clientId,
|
||||||
|
alias: siteResources.alias,
|
||||||
|
fullDomain: siteResources.fullDomain
|
||||||
|
})
|
||||||
|
.from(clientSiteResourcesAssociationsCache)
|
||||||
|
.innerJoin(
|
||||||
|
siteResources,
|
||||||
|
eq(
|
||||||
|
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||||
|
siteResources.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(
|
||||||
|
clientSiteResourcesAssociationsCache.clientId,
|
||||||
|
clientIdsWithExitNode
|
||||||
|
),
|
||||||
|
eq(siteResources.enabled, true),
|
||||||
|
eq(siteResources.requiresExitNodeConnection, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const needsConnectSet = new Set(
|
||||||
|
requiresExitNodeRows.map((r) => r.clientId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Aliases for every exit-node-backed resource this client can reach, so
|
||||||
|
// the live connect push carries the same alias list the register/reconnect
|
||||||
|
// path (buildSiteConfigurationForOlmClient) would compute.
|
||||||
|
const exitNodeAliasesByClientId = new Map<number, (string | null)[]>();
|
||||||
|
for (const row of requiresExitNodeRows) {
|
||||||
|
if (row.alias == null && row.fullDomain == null) continue;
|
||||||
|
const existing = exitNodeAliasesByClientId.get(row.clientId);
|
||||||
|
if (existing) {
|
||||||
|
existing.push(row.fullDomain || row.alias); // accept both for now in case we have other resource types that dont use the full domain
|
||||||
|
} else {
|
||||||
|
exitNodeAliasesByClientId.set(row.clientId, [
|
||||||
|
row.fullDomain || row.alias
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitNodeIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
clientsData
|
||||||
|
.map((c) => c.exitNodeId)
|
||||||
|
.filter((id): id is number => id !== null)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const exitNodeRows =
|
||||||
|
exitNodeIds.length > 0
|
||||||
|
? await trx
|
||||||
|
.select()
|
||||||
|
.from(exitNodes)
|
||||||
|
.where(inArray(exitNodes.exitNodeId, exitNodeIds))
|
||||||
|
: [];
|
||||||
|
const exitNodeById = new Map(exitNodeRows.map((n) => [n.exitNodeId, n]));
|
||||||
|
|
||||||
|
const olmRows = await trx
|
||||||
|
.select({
|
||||||
|
clientId: olms.clientId,
|
||||||
|
olmId: olms.olmId,
|
||||||
|
version: olms.version
|
||||||
|
})
|
||||||
|
.from(olms)
|
||||||
|
.where(inArray(olms.clientId, clientIdsWithExitNode));
|
||||||
|
const olmByClientId = new Map(
|
||||||
|
olmRows
|
||||||
|
.filter((r) => r.clientId !== null)
|
||||||
|
.map((r) => [r.clientId as number, r])
|
||||||
|
);
|
||||||
|
|
||||||
|
const relayPort = config.getRawConfig().gerbil.clients_start_port;
|
||||||
|
|
||||||
|
const connectPayloads: {
|
||||||
|
clientId: string;
|
||||||
|
message: { type: string; data: any };
|
||||||
|
options: { compress: boolean; incrementConfigVersion: boolean };
|
||||||
|
}[] = [];
|
||||||
|
const disconnectPayloads: {
|
||||||
|
clientId: string;
|
||||||
|
message: { type: string; data: any };
|
||||||
|
options: { compress: boolean; incrementConfigVersion: boolean };
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const client of clientsData) {
|
||||||
|
const olm = olmByClientId.get(client.clientId);
|
||||||
|
if (!olm) {
|
||||||
|
// No olm registered for this client yet/anymore, nothing to send.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsConnect = needsConnectSet.has(client.clientId);
|
||||||
|
|
||||||
|
if (needsConnect) {
|
||||||
|
const exitNode = client.exitNodeId
|
||||||
|
? exitNodeById.get(client.exitNodeId)
|
||||||
|
: undefined;
|
||||||
|
if (!exitNode || !client.exitNodeSubnet) {
|
||||||
|
logger.warn(
|
||||||
|
`rebuildClientAssociations: [syncClientExitNodeConnections] client ${client.clientId} needs an exit node connection but has no exit node or subnet assigned`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectPayloads.push({
|
||||||
|
clientId: olm.olmId,
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/exitnode/connect",
|
||||||
|
data: {
|
||||||
|
connect: true,
|
||||||
|
endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`,
|
||||||
|
relayPort,
|
||||||
|
publicKey: exitNode.publicKey,
|
||||||
|
serverIP: exitNode.address.split("/")[0],
|
||||||
|
tunnelIP: client.exitNodeSubnet.split("/")[0],
|
||||||
|
aliases:
|
||||||
|
exitNodeAliasesByClientId.get(client.clientId) ?? []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
compress: canCompress(olm.version, "olm"),
|
||||||
|
incrementConfigVersion: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
disconnectPayloads.push({
|
||||||
|
clientId: olm.olmId,
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/exitnode/disconnect",
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
compress: canCompress(olm.version, "olm"),
|
||||||
|
incrementConfigVersion: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectPayloads.length > 0) {
|
||||||
|
await sendToClientsBatch(connectPayloads).catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`rebuildClientAssociations: Error sending exit node connect messages:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disconnectPayloads.length > 0) {
|
||||||
|
await sendToClientsBatch(disconnectPayloads).catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`rebuildClientAssociations: Error sending exit node disconnect messages:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notifies the olms of every given client that the alias of the site resource
|
||||||
|
// they're using an exit node connection for has changed, via the dedicated
|
||||||
|
// exit node data-update message. Unlike syncClientExitNodeConnections, this
|
||||||
|
// doesn't touch connect/disconnect state - it's purely a rename for clients
|
||||||
|
// that are (and remain) connected to the exit node for this resource.
|
||||||
|
async function syncClientExitNodeAliasUpdate(
|
||||||
|
clientIds: number[],
|
||||||
|
oldAlias: string | null,
|
||||||
|
newAlias: string | null,
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<void> {
|
||||||
|
const uniqueClientIds = Array.from(new Set(clientIds));
|
||||||
|
if (uniqueClientIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldAliases = oldAlias ? [oldAlias] : [];
|
||||||
|
const newAliases = newAlias ? [newAlias] : [];
|
||||||
|
if (oldAliases.length === 0 && newAliases.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const olmRows = await trx
|
||||||
|
.select({
|
||||||
|
clientId: olms.clientId,
|
||||||
|
olmId: olms.olmId,
|
||||||
|
version: olms.version
|
||||||
|
})
|
||||||
|
.from(olms)
|
||||||
|
.where(inArray(olms.clientId, uniqueClientIds));
|
||||||
|
|
||||||
|
const updatePayloads = olmRows
|
||||||
|
.filter((r) => r.clientId !== null)
|
||||||
|
.map((olm) => ({
|
||||||
|
clientId: olm.olmId,
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/exitnode/data/update",
|
||||||
|
data: {
|
||||||
|
oldAliases,
|
||||||
|
newAliases
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
compress: canCompress(olm.version, "olm"),
|
||||||
|
incrementConfigVersion: true // this is important information we would need to sync
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (updatePayloads.length > 0) {
|
||||||
|
await sendToClientsBatch(updatePayloads).catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`rebuildClientAssociations: Error sending exit node alias update messages:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubnetProxyTargetUpdates(
|
async function handleSubnetProxyTargetUpdates(
|
||||||
siteResource: SiteResource,
|
siteResource: SiteResource,
|
||||||
sitesList: Site[],
|
sitesList: Site[],
|
||||||
@@ -1282,7 +1565,7 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
`handleMessagingForUpdatedSiteResource: fetched newts for ${newtsForSites.length}/${allSiteIds.length} site(s)`
|
`handleMessagingForUpdatedSiteResource: fetched newts for ${newtsForSites.length}/${allSiteIds.length} site(s)`
|
||||||
);
|
);
|
||||||
|
|
||||||
// WARNING: THIS RELIES ON THE CACHE TABLES BEING UP TO DATE, SO CALL THIS AFTER THE ASSOCIATION CACHE IS UPDATED
|
// !!!!!!!!!!!!!!!!!! WARNING: THIS RELIES ON THE CACHE TABLES BEING UP TO DATE, SO CALL THIS AFTER THE ASSOCIATION CACHE IS UPDATED !!!!!!!!!!!!!!!!!!
|
||||||
const mergedAllClients = await trx
|
const mergedAllClients = await trx
|
||||||
.select({
|
.select({
|
||||||
clientId: clientSiteResourcesAssociationsCache.clientId,
|
clientId: clientSiteResourcesAssociationsCache.clientId,
|
||||||
@@ -1709,6 +1992,38 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For a resource that stays on an exit node connection across the update,
|
||||||
|
// the alias is the only field that affects already-connected clients (the
|
||||||
|
// exit node itself, its endpoint, etc. are not per-resource). Tell those
|
||||||
|
// clients' olms about the rename directly via the exit node data-update
|
||||||
|
// message rather than a full connect/disconnect cycle.
|
||||||
|
if (
|
||||||
|
existingSiteResource?.requiresExitNodeConnection &&
|
||||||
|
updatedSiteResource.requiresExitNodeConnection &&
|
||||||
|
aliasChanged
|
||||||
|
) {
|
||||||
|
await syncClientExitNodeAliasUpdate(
|
||||||
|
mergedAllClients.map((c) => c.clientId),
|
||||||
|
existingSiteResource.alias,
|
||||||
|
updatedSiteResource.alias,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this resource requires (or required) clients to be connected to the
|
||||||
|
// exit node (e.g. an inference resource), re-sync connect/disconnect
|
||||||
|
// state for every client currently associated with it - covers toggling
|
||||||
|
// requiresExitNodeConnection on update as well as enabling/disabling it.
|
||||||
|
if (
|
||||||
|
updatedSiteResource.requiresExitNodeConnection ||
|
||||||
|
existingSiteResource?.requiresExitNodeConnection
|
||||||
|
) {
|
||||||
|
await syncClientExitNodeConnections(
|
||||||
|
mergedAllClients.map((c) => c.clientId),
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`handleMessagingForUpdatedSiteResource: DONE siteResourceId=${updatedSiteResource.siteResourceId}`
|
`handleMessagingForUpdatedSiteResource: DONE siteResourceId=${updatedSiteResource.siteResourceId}`
|
||||||
);
|
);
|
||||||
@@ -1990,6 +2305,10 @@ async function rebuildClientAssociationsFromClientImpl(
|
|||||||
resourcesToRemove,
|
resourcesToRemove,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Re-sync exit node connect/disconnect state based on this client's
|
||||||
|
// current full set of resource access (e.g. inference resources).
|
||||||
|
await syncClientExitNodeConnections([client.clientId], trx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMessagesForClientSites(
|
async function handleMessagesForClientSites(
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { db, resources, users, virtualApiKeyResources } from "@server/db";
|
||||||
|
import { and, asc, eq } from "drizzle-orm";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import { sendEmail } from "@server/emails";
|
||||||
|
import IdentityApiKeyGenerated from "@server/emails/templates/IdentityApiKeyGenerated";
|
||||||
|
import VirtualApiKeyGenerated from "@server/emails/templates/VirtualApiKeyGenerated";
|
||||||
|
import { formatVirtualApiKeyCredential } from "@server/lib/virtualApiKey";
|
||||||
|
|
||||||
|
const EMAIL_GATEWAY_URL_LIMIT = 5;
|
||||||
|
|
||||||
|
async function listVirtualApiKeyGatewayUrls(params: {
|
||||||
|
orgId: string;
|
||||||
|
allResources: boolean;
|
||||||
|
virtualApiKeyId: string;
|
||||||
|
}): Promise<{ urls: string[]; hasMore: boolean }> {
|
||||||
|
const rows = params.allResources
|
||||||
|
? await db
|
||||||
|
.select({
|
||||||
|
fullDomain: resources.fullDomain,
|
||||||
|
ssl: resources.ssl
|
||||||
|
})
|
||||||
|
.from(resources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.orgId, params.orgId),
|
||||||
|
eq(resources.mode, "inference")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(asc(resources.name))
|
||||||
|
.limit(EMAIL_GATEWAY_URL_LIMIT + 1)
|
||||||
|
: await db
|
||||||
|
.select({
|
||||||
|
fullDomain: resources.fullDomain,
|
||||||
|
ssl: resources.ssl
|
||||||
|
})
|
||||||
|
.from(virtualApiKeyResources)
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(virtualApiKeyResources.resourceId, resources.resourceId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
virtualApiKeyResources.virtualApiKeyId,
|
||||||
|
params.virtualApiKeyId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(asc(resources.name))
|
||||||
|
.limit(EMAIL_GATEWAY_URL_LIMIT + 1);
|
||||||
|
|
||||||
|
const urls = rows
|
||||||
|
.map((row) =>
|
||||||
|
row.fullDomain
|
||||||
|
? `${row.ssl ? "https" : "http"}://${row.fullDomain}`
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
.filter((url): url is string => Boolean(url));
|
||||||
|
|
||||||
|
return {
|
||||||
|
urls: urls.slice(0, EMAIL_GATEWAY_URL_LIMIT),
|
||||||
|
hasMore: rows.length > EMAIL_GATEWAY_URL_LIMIT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listOrgInferenceGatewayUrls(orgId: string): Promise<{
|
||||||
|
urls: string[];
|
||||||
|
hasMore: boolean;
|
||||||
|
}> {
|
||||||
|
return listVirtualApiKeyGatewayUrls({
|
||||||
|
orgId,
|
||||||
|
allResources: true,
|
||||||
|
virtualApiKeyId: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveVirtualApiKeyEmailRecipients(params: {
|
||||||
|
sendEmail: boolean;
|
||||||
|
sendToAttributedUser: boolean;
|
||||||
|
userId: string | null | undefined;
|
||||||
|
emails: string[];
|
||||||
|
}): Promise<
|
||||||
|
{ ok: true; recipients: string[] } | { ok: false; message: string }
|
||||||
|
> {
|
||||||
|
if (!params.sendEmail) {
|
||||||
|
return { ok: true, recipients: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.getRawConfig().email) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: "Email is not configured on this server"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipients = new Set(
|
||||||
|
params.emails.map((email) => email.trim().toLowerCase()).filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (params.sendToAttributedUser) {
|
||||||
|
if (!params.userId) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: "Associate a user to email the key to that user"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [user] = await db
|
||||||
|
.select({ email: users.email })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.userId, params.userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user?.email) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: "The associated user does not have an email address"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
recipients.add(user.email.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipients.size === 0) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: "Select at least one email recipient"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, recipients: [...recipients] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendVirtualApiKeyEmails(params: {
|
||||||
|
recipients: string[];
|
||||||
|
orgName: string;
|
||||||
|
orgId: string;
|
||||||
|
keyName: string | null;
|
||||||
|
virtualApiKeyId: string;
|
||||||
|
secret: string;
|
||||||
|
allResources: boolean;
|
||||||
|
isIdentityKey?: boolean;
|
||||||
|
accountLabel?: string | null;
|
||||||
|
gatewayUrls?: { urls: string[]; hasMore: boolean };
|
||||||
|
}): Promise<void> {
|
||||||
|
if (params.recipients.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const credential = formatVirtualApiKeyCredential(
|
||||||
|
params.virtualApiKeyId,
|
||||||
|
params.secret
|
||||||
|
);
|
||||||
|
const { urls, hasMore } =
|
||||||
|
params.gatewayUrls ??
|
||||||
|
(await listVirtualApiKeyGatewayUrls({
|
||||||
|
orgId: params.orgId,
|
||||||
|
allResources: params.allResources,
|
||||||
|
virtualApiKeyId: params.virtualApiKeyId
|
||||||
|
}));
|
||||||
|
const from = config.getNoReplyEmail();
|
||||||
|
const subject = params.isIdentityKey
|
||||||
|
? `Your identity key for ${params.orgName}`
|
||||||
|
: `Virtual API key for ${params.orgName}`;
|
||||||
|
|
||||||
|
for (const to of params.recipients) {
|
||||||
|
await sendEmail(
|
||||||
|
params.isIdentityKey
|
||||||
|
? IdentityApiKeyGenerated({
|
||||||
|
orgName: params.orgName,
|
||||||
|
accountLabel: params.accountLabel,
|
||||||
|
credential,
|
||||||
|
resourceUrls: urls,
|
||||||
|
hasMoreResources: hasMore
|
||||||
|
})
|
||||||
|
: VirtualApiKeyGenerated({
|
||||||
|
orgName: params.orgName,
|
||||||
|
keyName: params.keyName,
|
||||||
|
credential,
|
||||||
|
resourceUrls: urls,
|
||||||
|
hasMoreResources: hasMore
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
subject
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { db, exitNodes } from "@server/db";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
||||||
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
||||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
import { getValidCertificatesForDomains } from "@server/lib/certificates";
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
@@ -516,6 +516,11 @@ export class TraefikConfigManager {
|
|||||||
const maintenanceHost =
|
const maintenanceHost =
|
||||||
config.getRawConfig().server.internal_hostname;
|
config.getRawConfig().server.internal_hostname;
|
||||||
const pangolinUIUrl = `http://${maintenanceHost}:${maintenancePort}`;
|
const pangolinUIUrl = `http://${maintenanceHost}:${maintenancePort}`;
|
||||||
|
const aiGatewayUrl =
|
||||||
|
config.getRawConfig().server.ai_gateway_override ||
|
||||||
|
`http://${maintenanceHost}:${
|
||||||
|
config.getRawConfig().server.ai_gateway_port
|
||||||
|
}`;
|
||||||
|
|
||||||
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
|
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
|
||||||
traefikConfig = await getTraefikConfig(
|
traefikConfig = await getTraefikConfig(
|
||||||
@@ -528,7 +533,8 @@ export class TraefikConfigManager {
|
|||||||
? false
|
? false
|
||||||
: config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config
|
: config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config
|
||||||
pangolinUIUrl, // generate maintenance pages on cloud and hybrid
|
pangolinUIUrl, // generate maintenance pages on cloud and hybrid
|
||||||
pangolinUIUrl // generate browser gateway targets on cloud and hybrid
|
pangolinUIUrl, // generate browser gateway targets on cloud and hybrid
|
||||||
|
aiGatewayUrl
|
||||||
);
|
);
|
||||||
|
|
||||||
const domains = new Set<string>();
|
const domains = new Set<string>();
|
||||||
@@ -599,7 +605,30 @@ export class TraefikConfigManager {
|
|||||||
|
|
||||||
resourceSessionRequestParam:
|
resourceSessionRequestParam:
|
||||||
config.getRawConfig().server
|
config.getRawConfig().server
|
||||||
.resource_session_request_param
|
.resource_session_request_param,
|
||||||
|
|
||||||
|
remoteUserIdHeader:
|
||||||
|
config.getRawConfig().server.remote_headers
|
||||||
|
.user_id,
|
||||||
|
|
||||||
|
remoteVirtualApiKeyIdHeader:
|
||||||
|
config.getRawConfig().server.remote_headers
|
||||||
|
.virtual_api_key_id,
|
||||||
|
|
||||||
|
remoteUserHeader:
|
||||||
|
config.getRawConfig().server.remote_headers
|
||||||
|
.user,
|
||||||
|
|
||||||
|
remoteEmailHeader:
|
||||||
|
config.getRawConfig().server.remote_headers
|
||||||
|
.email,
|
||||||
|
|
||||||
|
remoteNameHeader:
|
||||||
|
config.getRawConfig().server.remote_headers
|
||||||
|
.name,
|
||||||
|
|
||||||
|
remoteRoleHeader:
|
||||||
|
config.getRawConfig().server.remote_headers.role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import config from "@server/lib/config";
|
||||||
|
import {
|
||||||
|
AI_GATEWAY_TRUST_HEADER,
|
||||||
|
AI_GATEWAY_RESOURCE_TYPE_HEADER,
|
||||||
|
AI_GATEWAY_CLIENT_IP_HEADER,
|
||||||
|
getAiGatewayTrustToken
|
||||||
|
} from "@server/lib/aiGatewayTrust";
|
||||||
|
|
||||||
|
// The trust token is the same for every inference route on an exit node, so
|
||||||
|
// these middlewares are built once and attached to each inference router.
|
||||||
|
// Two variants exist (public resource vs. siteResource) so the resource
|
||||||
|
// type header lets the gateway know which kind of router the request came
|
||||||
|
// through without re-deriving it from resourceId.
|
||||||
|
export const AI_GATEWAY_TRUST_MIDDLEWARE_RESOURCE =
|
||||||
|
"ai-gateway-trust-headers-resource";
|
||||||
|
export const AI_GATEWAY_TRUST_MIDDLEWARE_SITE_RESOURCE =
|
||||||
|
"ai-gateway-trust-headers-site-resource";
|
||||||
|
|
||||||
|
// Opt-in: a Badger instance with forward auth disabled, used only to stamp
|
||||||
|
// the resolved client IP into a dedicated header before the request reaches
|
||||||
|
// whatever sits between Traefik and the AI gateway. Only the site-resource
|
||||||
|
// router needs this - it's the only path that resolves request identity
|
||||||
|
// from the client IP (see resolveRequestUser in aiGateway/pipeline.ts) -
|
||||||
|
// and it's the only inference router that doesn't already run Badger.
|
||||||
|
export const AI_GATEWAY_CLIENT_IP_MIDDLEWARE_NAME = "ai-gateway-client-ip";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AI gateway may live on a different host than the inference resource
|
||||||
|
* itself (e.g. a remote exit node forwarding to the central dashboard over
|
||||||
|
* a tunnel), so callers use this to decide whether to pin the Host header
|
||||||
|
* to the gateway's own host.
|
||||||
|
*/
|
||||||
|
export function getAiGatewayHost(aiGatewayUrl: string): string | undefined {
|
||||||
|
try {
|
||||||
|
return new URL(aiGatewayUrl).host;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header middleware that pins the Host header to the AI gateway's own host
|
||||||
|
* (when it differs from the resource's) and smuggles the original resource
|
||||||
|
* host through in "p-host" instead, so passHostHeader can't leak the wrong
|
||||||
|
* Host to a gateway that lives on a different host than the resource.
|
||||||
|
*/
|
||||||
|
export function buildAiGatewayHostHeaderMiddleware(
|
||||||
|
aiGatewayHost: string | undefined,
|
||||||
|
fullDomain: string
|
||||||
|
): { headers: { customRequestHeaders: Record<string, string> } } {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: {
|
||||||
|
...(aiGatewayHost ? { Host: aiGatewayHost } : {}),
|
||||||
|
"p-host": fullDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAiGatewayTrustMiddlewares(): Record<string, any> {
|
||||||
|
const token = getAiGatewayTrustToken();
|
||||||
|
return {
|
||||||
|
[AI_GATEWAY_TRUST_MIDDLEWARE_RESOURCE]: {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: {
|
||||||
|
[AI_GATEWAY_TRUST_HEADER]: token,
|
||||||
|
[AI_GATEWAY_RESOURCE_TYPE_HEADER]: "resource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[AI_GATEWAY_TRUST_MIDDLEWARE_SITE_RESOURCE]: {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: {
|
||||||
|
[AI_GATEWAY_TRUST_HEADER]: token,
|
||||||
|
[AI_GATEWAY_RESOURCE_TYPE_HEADER]: "site-resource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAiGatewayClientIpMiddleware(): Record<string, any> | null {
|
||||||
|
const enabled =
|
||||||
|
config.getRawConfig().server.enable_ai_gateway_client_ip_header;
|
||||||
|
if (!enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
[AI_GATEWAY_CLIENT_IP_MIDDLEWARE_NAME]: {
|
||||||
|
plugin: {
|
||||||
|
badger: {
|
||||||
|
disableForwardAuth: true,
|
||||||
|
realIpHeader: AI_GATEWAY_CLIENT_IP_HEADER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the redirect (if ssl), main router, and single-server service for
|
||||||
|
* an AI-gateway-backed inference router. Identical between the public
|
||||||
|
* inference-resource and siteResource-inference cases, and between the OSS
|
||||||
|
* and private config generators - only the rule/tls/middleware chain
|
||||||
|
* differs, which callers resolve themselves beforehand.
|
||||||
|
*/
|
||||||
|
export function buildAiGatewayRouterAndService(params: {
|
||||||
|
routerName: string;
|
||||||
|
serviceName: string;
|
||||||
|
rule: string;
|
||||||
|
ssl: boolean | null;
|
||||||
|
tls: any;
|
||||||
|
priority: number;
|
||||||
|
routerMiddlewares: string[];
|
||||||
|
aiGatewayUrl: string;
|
||||||
|
redirectHttpsMiddlewareName: string;
|
||||||
|
}): { routers: Record<string, any>; services: Record<string, any> } {
|
||||||
|
const {
|
||||||
|
routerName,
|
||||||
|
serviceName,
|
||||||
|
rule,
|
||||||
|
ssl,
|
||||||
|
tls,
|
||||||
|
priority,
|
||||||
|
routerMiddlewares,
|
||||||
|
aiGatewayUrl,
|
||||||
|
redirectHttpsMiddlewareName
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const routers: Record<string, any> = {};
|
||||||
|
|
||||||
|
if (ssl) {
|
||||||
|
routers[`${routerName}-redirect`] = {
|
||||||
|
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
|
||||||
|
middlewares: [redirectHttpsMiddlewareName],
|
||||||
|
service: serviceName,
|
||||||
|
rule,
|
||||||
|
priority
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
routers[routerName] = {
|
||||||
|
entryPoints: [
|
||||||
|
ssl
|
||||||
|
? config.getRawConfig().traefik.https_entrypoint
|
||||||
|
: config.getRawConfig().traefik.http_entrypoint
|
||||||
|
],
|
||||||
|
middlewares: routerMiddlewares,
|
||||||
|
service: serviceName,
|
||||||
|
rule,
|
||||||
|
priority,
|
||||||
|
...(ssl ? { tls } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
const services = {
|
||||||
|
[serviceName]: {
|
||||||
|
loadBalancer: {
|
||||||
|
servers: [{ url: aiGatewayUrl }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { routers, services };
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
import config from "@server/lib/config";
|
||||||
|
import { sanitize } from "./utils";
|
||||||
|
|
||||||
|
export type BrowserGatewayResourceRow = {
|
||||||
|
resourceId: number;
|
||||||
|
resourceName: string | null;
|
||||||
|
mode: string;
|
||||||
|
fullDomain: string | null;
|
||||||
|
ssl: boolean | null;
|
||||||
|
subdomain: string | null;
|
||||||
|
domainId: string | null;
|
||||||
|
enabled: boolean | null;
|
||||||
|
wildcard: boolean | null;
|
||||||
|
domainCertResolver: string | null;
|
||||||
|
preferWildcardCert: boolean | null;
|
||||||
|
maintenanceModeEnabled: boolean | null;
|
||||||
|
maintenanceModeType: string | null;
|
||||||
|
maintenanceTitle: string | null;
|
||||||
|
maintenanceMessage: string | null;
|
||||||
|
maintenanceEstimatedTime: string | null;
|
||||||
|
targetId: number;
|
||||||
|
siteId: number;
|
||||||
|
siteType: string;
|
||||||
|
siteOnline: boolean | null;
|
||||||
|
subnet: string | null;
|
||||||
|
// Cloud-only namespace field - absent on OSS rows, so the namespace
|
||||||
|
// filter below naturally no-ops there.
|
||||||
|
domainNamespaceId?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BrowserGatewayResourceEntry = {
|
||||||
|
resourceId: number;
|
||||||
|
name: string;
|
||||||
|
fullDomain: string | null;
|
||||||
|
ssl: boolean | null;
|
||||||
|
subdomain: string | null;
|
||||||
|
domainId: string | null;
|
||||||
|
enabled: boolean | null;
|
||||||
|
wildcard: boolean | null;
|
||||||
|
domainCertResolver: string | null;
|
||||||
|
preferWildcardCert: boolean | null;
|
||||||
|
maintenanceModeEnabled: boolean | null;
|
||||||
|
maintenanceModeType: string | null;
|
||||||
|
maintenanceTitle: string | null;
|
||||||
|
maintenanceMessage: string | null;
|
||||||
|
maintenanceEstimatedTime: string | null;
|
||||||
|
targets: {
|
||||||
|
targetId: number;
|
||||||
|
bgType: string;
|
||||||
|
siteId: number;
|
||||||
|
siteType: string;
|
||||||
|
siteOnline: boolean | null;
|
||||||
|
subnet: string | null;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group the raw resource/target/site rows into per-resource browser-gateway
|
||||||
|
* entries (SSH/VNC/RDP-mode resources served through the browser gateway
|
||||||
|
* web UI instead of a real backend target).
|
||||||
|
*/
|
||||||
|
export function buildBrowserGatewayResourcesMap(
|
||||||
|
rows: BrowserGatewayResourceRow[],
|
||||||
|
filterOutNamespaceDomains: boolean
|
||||||
|
): Map<number, BrowserGatewayResourceEntry> {
|
||||||
|
const map = new Map<number, BrowserGatewayResourceEntry>();
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!["ssh", "vnc", "rdp"].includes(row.mode)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (filterOutNamespaceDomains && row.domainNamespaceId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!map.has(row.resourceId)) {
|
||||||
|
map.set(row.resourceId, {
|
||||||
|
resourceId: row.resourceId,
|
||||||
|
name: sanitize(row.resourceName ?? undefined) || "",
|
||||||
|
fullDomain: row.fullDomain,
|
||||||
|
ssl: row.ssl,
|
||||||
|
subdomain: row.subdomain,
|
||||||
|
domainId: row.domainId,
|
||||||
|
enabled: row.enabled,
|
||||||
|
wildcard: row.wildcard,
|
||||||
|
domainCertResolver: row.domainCertResolver,
|
||||||
|
preferWildcardCert: row.preferWildcardCert,
|
||||||
|
maintenanceModeEnabled: row.maintenanceModeEnabled,
|
||||||
|
maintenanceModeType: row.maintenanceModeType,
|
||||||
|
maintenanceTitle: row.maintenanceTitle,
|
||||||
|
maintenanceMessage: row.maintenanceMessage,
|
||||||
|
maintenanceEstimatedTime: row.maintenanceEstimatedTime,
|
||||||
|
targets: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
map.get(row.resourceId)!.targets.push({
|
||||||
|
targetId: row.targetId,
|
||||||
|
bgType: row.mode,
|
||||||
|
siteId: row.siteId,
|
||||||
|
siteType: row.siteType,
|
||||||
|
siteOnline: row.siteOnline,
|
||||||
|
subnet: row.subnet
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Traefik routers/services for browser-gateway resources
|
||||||
|
* (SSH/VNC/RDP served via a browser-based client instead of a raw target),
|
||||||
|
* mutating config_output. TLS/cert-resolver handling differs between the
|
||||||
|
* OSS (always resolve directly) and private (pangolin-dns aware) config
|
||||||
|
* generators, so callers resolve that themselves via resolveTls - returning
|
||||||
|
* null skips the resource (no valid cert available yet).
|
||||||
|
*/
|
||||||
|
export function buildBrowserGatewayConfig(params: {
|
||||||
|
config_output: any;
|
||||||
|
browserGatewayResourcesMap: Map<number, BrowserGatewayResourceEntry>;
|
||||||
|
browserGatewayUiUrl: string;
|
||||||
|
maintenancePageUiUrl: string | null;
|
||||||
|
badgerMiddlewareName: string;
|
||||||
|
redirectHttpsMiddlewareName: string;
|
||||||
|
resolveTls: (args: {
|
||||||
|
fullDomain: string;
|
||||||
|
hasSubdomain: boolean;
|
||||||
|
domainCertResolver: string | null;
|
||||||
|
preferWildcardCert: boolean | null;
|
||||||
|
}) => any | null;
|
||||||
|
}): void {
|
||||||
|
const {
|
||||||
|
config_output,
|
||||||
|
browserGatewayResourcesMap,
|
||||||
|
browserGatewayUiUrl,
|
||||||
|
maintenancePageUiUrl,
|
||||||
|
badgerMiddlewareName,
|
||||||
|
redirectHttpsMiddlewareName,
|
||||||
|
resolveTls
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const bgRateLimitMiddlewareName = "bg-ratelimit";
|
||||||
|
if (!config_output.http.middlewares) {
|
||||||
|
config_output.http.middlewares = {};
|
||||||
|
}
|
||||||
|
if (!config_output.http.middlewares[bgRateLimitMiddlewareName]) {
|
||||||
|
const traefikRateLimit = config.getRawConfig().traefik.rate_limit;
|
||||||
|
config_output.http.middlewares[bgRateLimitMiddlewareName] = {
|
||||||
|
rateLimit: {
|
||||||
|
average: traefikRateLimit.average,
|
||||||
|
burst: traefikRateLimit.burst
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const browserGatewayPort = 39999;
|
||||||
|
|
||||||
|
for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
|
||||||
|
if (!bgResource.enabled) continue;
|
||||||
|
if (!bgResource.domainId) continue;
|
||||||
|
if (!bgResource.fullDomain) continue;
|
||||||
|
|
||||||
|
if (!config_output.http.routers) config_output.http.routers = {};
|
||||||
|
if (!config_output.http.services) config_output.http.services = {};
|
||||||
|
|
||||||
|
const fullDomain = bgResource.fullDomain;
|
||||||
|
const additionalMiddlewares =
|
||||||
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
|
const routerMiddlewares = [
|
||||||
|
badgerMiddlewareName,
|
||||||
|
bgRateLimitMiddlewareName,
|
||||||
|
...additionalMiddlewares
|
||||||
|
];
|
||||||
|
|
||||||
|
const hostRule = `Host(\`${fullDomain}\`)`;
|
||||||
|
|
||||||
|
// Build TLS config
|
||||||
|
const tls = resolveTls({
|
||||||
|
fullDomain,
|
||||||
|
hasSubdomain: !!bgResource.subdomain,
|
||||||
|
domainCertResolver: bgResource.domainCertResolver,
|
||||||
|
preferWildcardCert: bgResource.preferWildcardCert
|
||||||
|
});
|
||||||
|
if (tls === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bgUiServiceName = `bg-r${bgResource.resourceId}-ui-service`;
|
||||||
|
|
||||||
|
if (bgResource.ssl) {
|
||||||
|
const redirectRouterName = `bg-r${bgResource.resourceId}-redirect`;
|
||||||
|
config_output.http.routers![redirectRouterName] = {
|
||||||
|
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
|
||||||
|
middlewares: [redirectHttpsMiddlewareName],
|
||||||
|
service: bgUiServiceName,
|
||||||
|
rule: hostRule,
|
||||||
|
priority: 100
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect online sites for this resource (for any type)
|
||||||
|
const anySiteOnline = bgResource.targets.some((t) => t.siteOnline);
|
||||||
|
|
||||||
|
// Maintenance page logic for browser gateway resources
|
||||||
|
let showBgMaintenancePage = false;
|
||||||
|
if (bgResource.maintenanceModeEnabled) {
|
||||||
|
if (bgResource.maintenanceModeType === "forced") {
|
||||||
|
showBgMaintenancePage = true;
|
||||||
|
} else if (bgResource.maintenanceModeType === "automatic") {
|
||||||
|
showBgMaintenancePage = !anySiteOnline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBgMaintenancePage && maintenancePageUiUrl) {
|
||||||
|
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`;
|
||||||
|
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`;
|
||||||
|
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`;
|
||||||
|
const bgMaintenanceHeadersMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-headers`;
|
||||||
|
|
||||||
|
const entrypointHttp =
|
||||||
|
config.getRawConfig().traefik.http_entrypoint;
|
||||||
|
const entrypointHttps =
|
||||||
|
config.getRawConfig().traefik.https_entrypoint;
|
||||||
|
|
||||||
|
if (!config_output.http.services) config_output.http.services = {};
|
||||||
|
if (!config_output.http.middlewares)
|
||||||
|
config_output.http.middlewares = {};
|
||||||
|
if (!config_output.http.routers) config_output.http.routers = {};
|
||||||
|
|
||||||
|
config_output.http.services![bgMaintenanceServiceName] = {
|
||||||
|
loadBalancer: {
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: maintenancePageUiUrl
|
||||||
|
}
|
||||||
|
],
|
||||||
|
passHostHeader: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config_output.http.middlewares![bgRewriteMiddlewareName] = {
|
||||||
|
replacePathRegex: {
|
||||||
|
regex: "^/(.*)",
|
||||||
|
replacement: "/maintenance-screen"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config_output.http.middlewares![
|
||||||
|
bgMaintenanceHeadersMiddlewareName
|
||||||
|
] = {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: {
|
||||||
|
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
|
||||||
|
"p-host": fullDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config_output.http.routers![bgMaintenanceRouterName] = {
|
||||||
|
entryPoints: [
|
||||||
|
bgResource.ssl ? entrypointHttps : entrypointHttp
|
||||||
|
],
|
||||||
|
service: bgMaintenanceServiceName,
|
||||||
|
middlewares: [
|
||||||
|
bgRewriteMiddlewareName,
|
||||||
|
bgMaintenanceHeadersMiddlewareName
|
||||||
|
],
|
||||||
|
rule: hostRule,
|
||||||
|
priority: 2000,
|
||||||
|
...(bgResource.ssl ? { tls } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Router to allow Next.js assets to load without rewrite
|
||||||
|
config_output.http.routers![`${bgMaintenanceRouterName}-assets`] = {
|
||||||
|
entryPoints: [
|
||||||
|
bgResource.ssl ? entrypointHttps : entrypointHttp
|
||||||
|
],
|
||||||
|
service: bgMaintenanceServiceName,
|
||||||
|
middlewares: [bgMaintenanceHeadersMiddlewareName],
|
||||||
|
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
|
||||||
|
priority: 2001,
|
||||||
|
...(bgResource.ssl ? { tls } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group targets by type and generate per-type websocket routers and services
|
||||||
|
const typeMap = new Map<string, typeof bgResource.targets>();
|
||||||
|
for (const t of bgResource.targets) {
|
||||||
|
if (!typeMap.has(t.bgType)) typeMap.set(t.bgType, []);
|
||||||
|
typeMap.get(t.bgType)!.push(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [bgType, typedTargets] of typeMap.entries()) {
|
||||||
|
const bgKey = `bg-r${bgResource.resourceId}-${bgType}`;
|
||||||
|
const bgRouterName = `${bgKey}-router`;
|
||||||
|
const bgServiceName = `${bgKey}-service`;
|
||||||
|
const bgRule = `${hostRule} && PathPrefix(\`/gateway/${bgType}\`)`;
|
||||||
|
|
||||||
|
const servers = typedTargets
|
||||||
|
.filter((t) => {
|
||||||
|
if (!t.siteOnline && anySiteOnline) return false;
|
||||||
|
if (t.siteType === "newt") return !!t.subnet;
|
||||||
|
return false; // browser gateway only supported on newt sites
|
||||||
|
})
|
||||||
|
.map((t) => ({
|
||||||
|
url: `http://${t.subnet!.split("/")[0]}:${browserGatewayPort}`
|
||||||
|
}))
|
||||||
|
.filter((v, i, a) => a.findIndex((u) => u.url === v.url) === i);
|
||||||
|
|
||||||
|
config_output.http.routers![bgRouterName] = {
|
||||||
|
entryPoints: [
|
||||||
|
bgResource.ssl
|
||||||
|
? config.getRawConfig().traefik.https_entrypoint
|
||||||
|
: config.getRawConfig().traefik.http_entrypoint
|
||||||
|
],
|
||||||
|
middlewares: routerMiddlewares,
|
||||||
|
service: bgServiceName,
|
||||||
|
rule: bgRule,
|
||||||
|
priority: 110, // highest - websocket path takes precedence
|
||||||
|
...(bgResource.ssl ? { tls } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
config_output.http.services![bgServiceName] = {
|
||||||
|
loadBalancer: {
|
||||||
|
servers
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI: serve the browser gateway page from the internal pangolin instance.
|
||||||
|
// The primary type is used for the path rewrite (e.g. /rdp), mirroring
|
||||||
|
// how the maintenance page rewrites everything to /maintenance-screen.
|
||||||
|
const primaryType = typeMap.keys().next().value as string;
|
||||||
|
const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`;
|
||||||
|
const uiHeadersMiddlewareName = `bg-r${bgResource.resourceId}-ui-headers`;
|
||||||
|
const entrypoint = bgResource.ssl
|
||||||
|
? config.getRawConfig().traefik.https_entrypoint
|
||||||
|
: config.getRawConfig().traefik.http_entrypoint;
|
||||||
|
|
||||||
|
if (!config_output.http.middlewares) {
|
||||||
|
config_output.http.middlewares = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
config_output.http.middlewares![uiRewriteMiddlewareName] = {
|
||||||
|
replacePathRegex: {
|
||||||
|
regex: "^/(.*)",
|
||||||
|
replacement: `/${primaryType}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config_output.http.middlewares![uiHeadersMiddlewareName] = {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: {
|
||||||
|
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
|
||||||
|
"p-host": fullDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config_output.http.services![bgUiServiceName] = {
|
||||||
|
loadBalancer: {
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: browserGatewayUiUrl
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assets router at higher priority so /_next files load without rewrite.
|
||||||
|
// Do NOT apply the path-rewrite middleware here — static assets must
|
||||||
|
// keep their original path; only the host headers are needed.
|
||||||
|
config_output.http.routers![
|
||||||
|
`bg-r${bgResource.resourceId}-assets-router`
|
||||||
|
] = {
|
||||||
|
entryPoints: [entrypoint],
|
||||||
|
middlewares: [...routerMiddlewares, uiHeadersMiddlewareName],
|
||||||
|
service: bgUiServiceName,
|
||||||
|
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
|
||||||
|
priority: 101,
|
||||||
|
...(bgResource.ssl ? { tls } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Catch-all router rewrites everything on the domain to /{primaryType}
|
||||||
|
config_output.http.routers![`bg-r${bgResource.resourceId}-ui-router`] =
|
||||||
|
{
|
||||||
|
entryPoints: [entrypoint],
|
||||||
|
middlewares: [
|
||||||
|
...routerMiddlewares,
|
||||||
|
uiRewriteMiddlewareName,
|
||||||
|
uiHeadersMiddlewareName
|
||||||
|
],
|
||||||
|
service: bgUiServiceName,
|
||||||
|
rule: hostRule,
|
||||||
|
priority: 100,
|
||||||
|
...(bgResource.ssl ? { tls } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Traefik `tls` block for a domain using the cert-resolver /
|
||||||
|
* wildcard-cert logic shared by both the OSS and private Traefik config
|
||||||
|
* generators (used whenever certs are obtained directly via ACME rather
|
||||||
|
* than through pangolin-dns).
|
||||||
|
*/
|
||||||
|
export function buildWildcardTls(params: {
|
||||||
|
fullDomain: string;
|
||||||
|
hasSubdomain: boolean;
|
||||||
|
domainCertResolver?: string | null;
|
||||||
|
preferWildcardCert?: boolean | null;
|
||||||
|
}): { certResolver: string | undefined; domains?: { main: string }[] } {
|
||||||
|
const { fullDomain, hasSubdomain, domainCertResolver, preferWildcardCert } =
|
||||||
|
params;
|
||||||
|
|
||||||
|
const domainParts = fullDomain.split(".");
|
||||||
|
let wildCard =
|
||||||
|
domainParts.length <= 2
|
||||||
|
? `*.${domainParts.join(".")}`
|
||||||
|
: `*.${domainParts.slice(1).join(".")}`;
|
||||||
|
if (!hasSubdomain) {
|
||||||
|
wildCard = fullDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalDefaultResolver = config.getRawConfig().traefik.cert_resolver;
|
||||||
|
const globalDefaultPreferWildcard =
|
||||||
|
config.getRawConfig().traefik.prefer_wildcard_cert;
|
||||||
|
|
||||||
|
const resolverName = domainCertResolver
|
||||||
|
? domainCertResolver.trim()
|
||||||
|
: globalDefaultResolver;
|
||||||
|
|
||||||
|
const preferWildcard =
|
||||||
|
preferWildcardCert !== undefined && preferWildcardCert !== null
|
||||||
|
? preferWildcardCert
|
||||||
|
: globalDefaultPreferWildcard;
|
||||||
|
|
||||||
|
return {
|
||||||
|
certResolver: resolverName,
|
||||||
|
...(preferWildcard ? { domains: [{ main: wildCard }] } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
import { db, targetHealthCheck, domains } from "@server/db";
|
import {
|
||||||
|
db,
|
||||||
|
targetHealthCheck,
|
||||||
|
domains,
|
||||||
|
aiProviders,
|
||||||
|
resourceAiProviders,
|
||||||
|
siteResources,
|
||||||
|
siteNetworks,
|
||||||
|
exitNodes
|
||||||
|
} from "@server/db";
|
||||||
import {
|
import {
|
||||||
and,
|
and,
|
||||||
eq,
|
eq,
|
||||||
@@ -12,41 +21,64 @@ import {
|
|||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { resources, sites, Target, targets } from "@server/db";
|
import { resources, sites, targets } from "@server/db";
|
||||||
import createPathRewriteMiddleware from "./middleware";
|
import { applyPathRewriteMiddleware } from "./middleware";
|
||||||
import { sanitize, encodePath, validatePathRewriteConfig } from "./utils";
|
import { sanitize, encodePath, validatePathRewriteConfig } from "./utils";
|
||||||
|
import regionalCache from "@server/lib/cache";
|
||||||
|
import { TargetWithSite } from "./types";
|
||||||
|
import { buildWildcardTls } from "./certResolver";
|
||||||
|
import { buildHostRule, appendPathMatch, computeRoutePriority } from "./rule";
|
||||||
|
import {
|
||||||
|
buildHttpLoadBalancerServers,
|
||||||
|
buildStickySessionCookie,
|
||||||
|
buildTcpUdpLoadBalancerServers,
|
||||||
|
buildStickySessionIp
|
||||||
|
} from "./loadBalancer";
|
||||||
|
import { buildCustomHeadersMiddleware } from "./headersMiddleware";
|
||||||
|
import {
|
||||||
|
AI_GATEWAY_TRUST_MIDDLEWARE_RESOURCE,
|
||||||
|
AI_GATEWAY_TRUST_MIDDLEWARE_SITE_RESOURCE,
|
||||||
|
AI_GATEWAY_CLIENT_IP_MIDDLEWARE_NAME,
|
||||||
|
getAiGatewayHost,
|
||||||
|
buildAiGatewayTrustMiddlewares,
|
||||||
|
buildAiGatewayClientIpMiddleware,
|
||||||
|
buildAiGatewayHostHeaderMiddleware,
|
||||||
|
buildAiGatewayRouterAndService
|
||||||
|
} from "./aiGatewayMiddlewares";
|
||||||
|
import {
|
||||||
|
buildBrowserGatewayResourcesMap,
|
||||||
|
buildBrowserGatewayConfig
|
||||||
|
} from "./browserGateway";
|
||||||
|
import { buildSiteResourceAliasCertPlaceholders } from "./siteResourceAlias";
|
||||||
|
|
||||||
const redirectHttpsMiddlewareName = "redirect-to-https";
|
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||||
const badgerMiddlewareName = "badger";
|
const badgerMiddlewareName = "badger";
|
||||||
|
|
||||||
// Define extended target type with site information
|
|
||||||
type TargetWithSite = Target & {
|
|
||||||
resourceId: number;
|
|
||||||
targetId: number;
|
|
||||||
ip: string | null;
|
|
||||||
method: string | null;
|
|
||||||
port: number | null;
|
|
||||||
internalPort: number | null;
|
|
||||||
enabled: boolean;
|
|
||||||
health: string | null;
|
|
||||||
site: {
|
|
||||||
siteId: number;
|
|
||||||
type: string;
|
|
||||||
subnet: string | null;
|
|
||||||
exitNodeId: number | null;
|
|
||||||
online: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getTraefikConfig(
|
export async function getTraefikConfig(
|
||||||
exitNodeId: number,
|
exitNodeId: number,
|
||||||
siteTypes: string[],
|
siteTypes: string[],
|
||||||
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
|
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
|
||||||
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
|
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
|
||||||
allowRawResources = true,
|
allowRawResources = true,
|
||||||
maintenancePageUiUrl: string | null = null, // UNUSED BUT USED IN PRIVATE
|
maintenancePageUiUrl: string | null = null,
|
||||||
browserGatewayUiUrl: string | null = null // UNUSED BUT USED IN PRIVATE
|
browserGatewayUiUrl: string | null = null,
|
||||||
|
aiGatewayUrl: string | null = null
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
// Get the exit node but cache it for 5 minutes to avoid hitting the DB too often
|
||||||
|
const exitNodeCacheKey = `exitNode:${exitNodeId}`;
|
||||||
|
let exitNode =
|
||||||
|
await regionalCache.get<typeof exitNodes.$inferSelect>(
|
||||||
|
exitNodeCacheKey
|
||||||
|
);
|
||||||
|
if (!exitNode) {
|
||||||
|
[exitNode] = await db
|
||||||
|
.select()
|
||||||
|
.from(exitNodes)
|
||||||
|
.where(eq(exitNodes.exitNodeId, exitNodeId))
|
||||||
|
.limit(1);
|
||||||
|
await regionalCache.set(exitNodeCacheKey, exitNode, 300);
|
||||||
|
}
|
||||||
|
|
||||||
// Get resources with their targets and sites in a single optimized query
|
// Get resources with their targets and sites in a single optimized query
|
||||||
// Start from sites on this exit node, then join to targets and resources
|
// Start from sites on this exit node, then join to targets and resources
|
||||||
const resourcesWithTargetsAndSites = await db
|
const resourcesWithTargetsAndSites = await db
|
||||||
@@ -67,8 +99,15 @@ export async function getTraefikConfig(
|
|||||||
headers: resources.headers,
|
headers: resources.headers,
|
||||||
proxyProtocol: resources.proxyProtocol,
|
proxyProtocol: resources.proxyProtocol,
|
||||||
proxyProtocolVersion: resources.proxyProtocolVersion,
|
proxyProtocolVersion: resources.proxyProtocolVersion,
|
||||||
|
wildcard: resources.wildcard,
|
||||||
mode: resources.mode,
|
mode: resources.mode,
|
||||||
|
|
||||||
|
maintenanceModeEnabled: resources.maintenanceModeEnabled,
|
||||||
|
maintenanceModeType: resources.maintenanceModeType,
|
||||||
|
maintenanceTitle: resources.maintenanceTitle,
|
||||||
|
maintenanceMessage: resources.maintenanceMessage,
|
||||||
|
maintenanceEstimatedTime: resources.maintenanceEstimatedTime,
|
||||||
|
|
||||||
// Target fields
|
// Target fields
|
||||||
targetId: targets.targetId,
|
targetId: targets.targetId,
|
||||||
targetEnabled: targets.enabled,
|
targetEnabled: targets.enabled,
|
||||||
@@ -87,7 +126,7 @@ export async function getTraefikConfig(
|
|||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
siteType: sites.type,
|
siteType: sites.type,
|
||||||
siteOnline: sites.online,
|
siteOnline: sites.online,
|
||||||
subnet: sites.subnet,
|
subnet: sites.exitNodeSubnet,
|
||||||
exitNodeId: sites.exitNodeId,
|
exitNodeId: sites.exitNodeId,
|
||||||
// Domain cert resolver fields
|
// Domain cert resolver fields
|
||||||
domainCertResolver: domains.certResolver,
|
domainCertResolver: domains.certResolver,
|
||||||
@@ -115,8 +154,15 @@ export async function getTraefikConfig(
|
|||||||
),
|
),
|
||||||
inArray(sites.type, siteTypes),
|
inArray(sites.type, siteTypes),
|
||||||
allowRawResources
|
allowRawResources
|
||||||
? inArray(resources.mode, ["http", "udp", "tcp"]) // allow all three
|
? inArray(resources.mode, [
|
||||||
: eq(resources.mode, "http")
|
"http",
|
||||||
|
"udp",
|
||||||
|
"tcp",
|
||||||
|
"vnc",
|
||||||
|
"ssh",
|
||||||
|
"rdp"
|
||||||
|
]) // allow all three, plus browser-gateway modes
|
||||||
|
: inArray(resources.mode, ["http", "vnc", "ssh", "rdp"])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.orderBy(desc(targets.priority), targets.targetId); // stable ordering
|
.orderBy(desc(targets.priority), targets.targetId); // stable ordering
|
||||||
@@ -125,6 +171,9 @@ export async function getTraefikConfig(
|
|||||||
const resourcesMap = new Map();
|
const resourcesMap = new Map();
|
||||||
|
|
||||||
resourcesWithTargetsAndSites.forEach((row) => {
|
resourcesWithTargetsAndSites.forEach((row) => {
|
||||||
|
if (!["http", "tcp", "udp"].includes(row.mode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const resourceId = row.resourceId;
|
const resourceId = row.resourceId;
|
||||||
const resourceName = sanitize(row.resourceName) || "";
|
const resourceName = sanitize(row.resourceName) || "";
|
||||||
const targetPath = encodePath(row.path); // Use encodePath to avoid collisions (e.g. "/a/b" vs "/a-b")
|
const targetPath = encodePath(row.path); // Use encodePath to avoid collisions (e.g. "/a/b" vs "/a-b")
|
||||||
@@ -209,8 +258,81 @@ export async function getTraefikConfig(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Group browser gateway targets by resource (SSH/VNC/RDP-mode resources
|
||||||
|
// served through the browser gateway web UI instead of a real target).
|
||||||
|
const browserGatewayResourcesMap = browserGatewayUiUrl
|
||||||
|
? buildBrowserGatewayResourcesMap(
|
||||||
|
resourcesWithTargetsAndSites,
|
||||||
|
filterOutNamespaceDomains
|
||||||
|
)
|
||||||
|
: new Map();
|
||||||
|
|
||||||
|
// Query siteResources in HTTP mode with SSL enabled and aliases, so
|
||||||
|
// Traefik generates TLS certificates for those domains even before a
|
||||||
|
// matching resource exists.
|
||||||
|
const siteResourcesWithFullDomain = await db
|
||||||
|
.select({
|
||||||
|
siteResourceId: siteResources.siteResourceId,
|
||||||
|
fullDomain: siteResources.fullDomain
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.innerJoin(
|
||||||
|
siteNetworks,
|
||||||
|
eq(siteResources.networkId, siteNetworks.networkId)
|
||||||
|
)
|
||||||
|
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.enabled, true),
|
||||||
|
isNotNull(siteResources.fullDomain),
|
||||||
|
eq(siteResources.mode, "http"), // important so we dont double get the inference siteResources below
|
||||||
|
eq(siteResources.ssl, true),
|
||||||
|
eq(sites.exitNodeId, exitNodeId),
|
||||||
|
inArray(sites.type, siteTypes)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Inference-mode resources have no targets/sites (their "backend" is the
|
||||||
|
// central AI gateway), so they can't be reached via the targets->sites
|
||||||
|
// join above - query them separately and include them on every exit node.
|
||||||
|
const inferenceResources = await db
|
||||||
|
.selectDistinct({
|
||||||
|
resourceId: resources.resourceId,
|
||||||
|
resourceName: resources.name,
|
||||||
|
fullDomain: resources.fullDomain,
|
||||||
|
ssl: resources.ssl,
|
||||||
|
subdomain: resources.subdomain,
|
||||||
|
domainId: resources.domainId,
|
||||||
|
enabled: resources.enabled,
|
||||||
|
wildcard: resources.wildcard,
|
||||||
|
domainCertResolver: domains.certResolver,
|
||||||
|
preferWildcardCert: domains.preferWildcardCert
|
||||||
|
})
|
||||||
|
.from(resources)
|
||||||
|
// .innerJoin(
|
||||||
|
// resourceAiProviders,
|
||||||
|
// eq(resources.resourceId, resourceAiProviders.resourceId)
|
||||||
|
// )
|
||||||
|
// .innerJoin(
|
||||||
|
// aiProviders,
|
||||||
|
// eq(resourceAiProviders.providerId, aiProviders.providerId)
|
||||||
|
// )
|
||||||
|
.leftJoin(domains, eq(domains.domainId, resources.domainId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.mode, "inference"),
|
||||||
|
eq(resources.enabled, true)
|
||||||
|
// eq(aiProviders.enabled, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// make sure we have at least one resource
|
// make sure we have at least one resource
|
||||||
if (resourcesMap.size === 0) {
|
if (
|
||||||
|
resourcesMap.size === 0 &&
|
||||||
|
inferenceResources.length === 0 &&
|
||||||
|
browserGatewayResourcesMap.size === 0 &&
|
||||||
|
siteResourcesWithFullDomain.length === 0
|
||||||
|
) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,56 +376,12 @@ export async function getTraefikConfig(
|
|||||||
config_output.http.services = {};
|
config_output.http.services = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const domainParts = fullDomain.split(".");
|
const tls = buildWildcardTls({
|
||||||
let wildCard;
|
fullDomain,
|
||||||
if (domainParts.length <= 2) {
|
hasSubdomain: !!resource.subdomain,
|
||||||
wildCard = `*.${domainParts.join(".")}`;
|
domainCertResolver: resource.domainCertResolver,
|
||||||
} else {
|
preferWildcardCert: resource.preferWildcardCert
|
||||||
wildCard = `*.${domainParts.slice(1).join(".")}`;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (!resource.subdomain) {
|
|
||||||
wildCard = resource.fullDomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalDefaultResolver =
|
|
||||||
config.getRawConfig().traefik.cert_resolver;
|
|
||||||
const globalDefaultPreferWildcard =
|
|
||||||
config.getRawConfig().traefik.prefer_wildcard_cert;
|
|
||||||
|
|
||||||
const domainCertResolver = resource.domainCertResolver;
|
|
||||||
const preferWildcardCert = resource.preferWildcardCert;
|
|
||||||
|
|
||||||
let resolverName: string | undefined;
|
|
||||||
let preferWildcard: boolean | undefined;
|
|
||||||
// Handle both letsencrypt & custom cases
|
|
||||||
if (domainCertResolver) {
|
|
||||||
resolverName = domainCertResolver.trim();
|
|
||||||
} else {
|
|
||||||
resolverName = globalDefaultResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
preferWildcardCert !== undefined &&
|
|
||||||
preferWildcardCert !== null
|
|
||||||
) {
|
|
||||||
preferWildcard = preferWildcardCert;
|
|
||||||
} else {
|
|
||||||
preferWildcard = globalDefaultPreferWildcard;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tls = {
|
|
||||||
certResolver: resolverName,
|
|
||||||
...(preferWildcard
|
|
||||||
? {
|
|
||||||
domains: [
|
|
||||||
{
|
|
||||||
main: wildCard
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
};
|
|
||||||
|
|
||||||
const additionalMiddlewares =
|
const additionalMiddlewares =
|
||||||
config.getRawConfig().traefik.additional_middlewares || [];
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
@@ -314,134 +392,40 @@ export async function getTraefikConfig(
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Handle path rewriting middleware
|
// Handle path rewriting middleware
|
||||||
if (
|
applyPathRewriteMiddleware(
|
||||||
resource.rewritePath !== null &&
|
config_output,
|
||||||
resource.path !== null &&
|
resource.resourceId,
|
||||||
resource.pathMatchType &&
|
key,
|
||||||
resource.rewritePathType
|
|
||||||
) {
|
|
||||||
// Create a unique middleware name
|
|
||||||
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rewriteResult = createPathRewriteMiddleware(
|
|
||||||
rewriteMiddlewareName,
|
|
||||||
resource.path,
|
resource.path,
|
||||||
resource.pathMatchType,
|
resource.pathMatchType,
|
||||||
resource.rewritePath,
|
resource.rewritePath,
|
||||||
resource.rewritePathType
|
resource.rewritePathType,
|
||||||
|
routerMiddlewares
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize middlewares object if it doesn't exist
|
|
||||||
if (!config_output.http.middlewares) {
|
|
||||||
config_output.http.middlewares = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// the middleware to the config
|
|
||||||
Object.assign(
|
|
||||||
config_output.http.middlewares,
|
|
||||||
rewriteResult.middlewares
|
|
||||||
);
|
|
||||||
|
|
||||||
// middlewares to the router middleware chain
|
|
||||||
if (rewriteResult.chain) {
|
|
||||||
// For chained middlewares (like stripPrefix + addPrefix)
|
|
||||||
routerMiddlewares.push(...rewriteResult.chain);
|
|
||||||
} else {
|
|
||||||
// Single middleware
|
|
||||||
routerMiddlewares.push(rewriteMiddlewareName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// logger.debug(
|
|
||||||
// `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
|
||||||
// );
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle custom headers middleware
|
// Handle custom headers middleware
|
||||||
if (resource.headers || resource.setHostHeader) {
|
const customHeadersMiddleware = buildCustomHeadersMiddleware(
|
||||||
const headersObj: { [key: string]: string } = {};
|
resource.headers,
|
||||||
|
resource.setHostHeader,
|
||||||
if (resource.headers) {
|
resource.resourceId
|
||||||
let headersArr: { name: string; value: string }[] = [];
|
|
||||||
try {
|
|
||||||
headersArr = JSON.parse(resource.headers) as {
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
}[];
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(
|
|
||||||
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
|
|
||||||
);
|
);
|
||||||
}
|
if (customHeadersMiddleware) {
|
||||||
|
|
||||||
headersArr.forEach((header) => {
|
|
||||||
headersObj[header.name] = header.value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resource.setHostHeader) {
|
|
||||||
headersObj["Host"] = resource.setHostHeader;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(headersObj).length > 0) {
|
|
||||||
if (!config_output.http.middlewares) {
|
if (!config_output.http.middlewares) {
|
||||||
config_output.http.middlewares = {};
|
config_output.http.middlewares = {};
|
||||||
}
|
}
|
||||||
config_output.http.middlewares[headersMiddlewareName] = {
|
config_output.http.middlewares[headersMiddlewareName] =
|
||||||
headers: {
|
customHeadersMiddleware;
|
||||||
customRequestHeaders: headersObj
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
routerMiddlewares.push(headersMiddlewareName);
|
routerMiddlewares.push(headersMiddlewareName);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Build routing rules
|
// Build routing rules
|
||||||
let rule = `Host(\`${fullDomain}\`)`;
|
let rule = buildHostRule(fullDomain);
|
||||||
|
const priority = computeRoutePriority(
|
||||||
// priority logic
|
resource.priority,
|
||||||
let priority: number;
|
resource.path,
|
||||||
if (resource.priority && resource.priority != 100) {
|
resource.pathMatchType
|
||||||
priority = resource.priority;
|
);
|
||||||
} else {
|
rule = appendPathMatch(rule, resource.path, resource.pathMatchType);
|
||||||
priority = 100;
|
|
||||||
if (resource.path && resource.pathMatchType) {
|
|
||||||
priority += 10;
|
|
||||||
if (resource.pathMatchType === "exact") {
|
|
||||||
priority += 5;
|
|
||||||
} else if (resource.pathMatchType === "prefix") {
|
|
||||||
priority += 3;
|
|
||||||
} else if (resource.pathMatchType === "regex") {
|
|
||||||
priority += 2;
|
|
||||||
}
|
|
||||||
if (resource.path === "/") {
|
|
||||||
priority = 1; // lowest for catch-all
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resource.path && resource.pathMatchType) {
|
|
||||||
// priority += 1;
|
|
||||||
// add path to rule based on match type
|
|
||||||
let path = resource.path;
|
|
||||||
// if the path doesn't start with a /, add it
|
|
||||||
if (!path.startsWith("/")) {
|
|
||||||
path = `/${path}`;
|
|
||||||
}
|
|
||||||
if (resource.pathMatchType === "exact") {
|
|
||||||
rule += ` && Path(\`${path}\`)`;
|
|
||||||
} else if (resource.pathMatchType === "prefix") {
|
|
||||||
rule += ` && PathPrefix(\`${path}\`)`;
|
|
||||||
} else if (resource.pathMatchType === "regex") {
|
|
||||||
rule += ` && PathRegexp(\`${resource.path}\`)`; // this is the raw path because it's a regex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config_output.http.routers![routerName] = {
|
config_output.http.routers![routerName] = {
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
@@ -470,90 +454,9 @@ export async function getTraefikConfig(
|
|||||||
|
|
||||||
config_output.http.services![serviceName] = {
|
config_output.http.services![serviceName] = {
|
||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: (() => {
|
servers: buildHttpLoadBalancerServers(targets),
|
||||||
// Check if any sites are online
|
|
||||||
// THIS IS SO THAT THERE IS SOME IMMEDIATE FEEDBACK
|
|
||||||
// EVEN IF THE SITES HAVE NOT UPDATED YET FROM THE
|
|
||||||
// RECEIVE BANDWIDTH ENDPOINT.
|
|
||||||
|
|
||||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
|
||||||
const anySitesOnline = targets.some(
|
|
||||||
(target) => target.site.online
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
targets
|
|
||||||
.filter((target) => {
|
|
||||||
if (!target.enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.health == "unhealthy") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any sites are online, exclude offline sites
|
|
||||||
if (anySitesOnline && !target.site.online) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
target.site.type === "local" ||
|
|
||||||
target.site.type === "wireguard"
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
!target.ip ||
|
|
||||||
!target.port ||
|
|
||||||
!target.method
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (target.site.type === "newt") {
|
|
||||||
if (
|
|
||||||
!target.internalPort ||
|
|
||||||
!target.method ||
|
|
||||||
!target.site.subnet
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map((target) => {
|
|
||||||
if (
|
|
||||||
target.site.type === "local" ||
|
|
||||||
target.site.type === "wireguard"
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
url: `${target.method}://${target.ip}:${target.port}`
|
|
||||||
};
|
|
||||||
} else if (target.site.type === "newt") {
|
|
||||||
const ip =
|
|
||||||
target.site.subnet!.split("/")[0];
|
|
||||||
return {
|
|
||||||
url: `${target.method}://${ip}:${target.internalPort}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// filter out duplicates
|
|
||||||
.filter(
|
|
||||||
(v, i, a) =>
|
|
||||||
a.findIndex(
|
|
||||||
(t) => t && v && t.url === v.url
|
|
||||||
) === i
|
|
||||||
)
|
|
||||||
);
|
|
||||||
})(),
|
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? buildStickySessionCookie(resource.ssl)
|
||||||
sticky: {
|
|
||||||
cookie: {
|
|
||||||
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
|
||||||
secure: resource.ssl,
|
|
||||||
httpOnly: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -603,75 +506,249 @@ export async function getTraefikConfig(
|
|||||||
|
|
||||||
config_output[protocol].services[serviceName] = {
|
config_output[protocol].services[serviceName] = {
|
||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: (() => {
|
servers: buildTcpUdpLoadBalancerServers(targets),
|
||||||
// Check if any sites are online
|
|
||||||
const anySitesOnline = targets.some(
|
|
||||||
(target) => target.site.online
|
|
||||||
);
|
|
||||||
|
|
||||||
return targets
|
|
||||||
.filter((target) => {
|
|
||||||
if (!target.enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any sites are online, exclude offline sites
|
|
||||||
if (anySitesOnline && !target.site.online) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
target.site.type === "local" ||
|
|
||||||
target.site.type === "wireguard"
|
|
||||||
) {
|
|
||||||
if (!target.ip || !target.port) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (target.site.type === "newt") {
|
|
||||||
if (
|
|
||||||
!target.internalPort ||
|
|
||||||
!target.site.subnet
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map((target) => {
|
|
||||||
if (
|
|
||||||
target.site.type === "local" ||
|
|
||||||
target.site.type === "wireguard"
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
address: `${target.ip}:${target.port}`
|
|
||||||
};
|
|
||||||
} else if (target.site.type === "newt") {
|
|
||||||
const ip =
|
|
||||||
target.site.subnet!.split("/")[0];
|
|
||||||
return {
|
|
||||||
address: `${ip}:${target.internalPort}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})(),
|
|
||||||
...(resource.proxyProtocol && protocol == "tcp"
|
...(resource.proxyProtocol && protocol == "tcp"
|
||||||
? {
|
? {
|
||||||
serversTransport: `${ppPrefix}${resource.proxyProtocolVersion || 1}@file` // TODO: does @file here cause issues?
|
serversTransport: `${ppPrefix}${resource.proxyProtocolVersion || 1}@file` // TODO: does @file here cause issues?
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(resource.stickySession
|
...(resource.stickySession ? buildStickySessionIp() : {})
|
||||||
? {
|
|
||||||
sticky: {
|
|
||||||
ipStrategy: {
|
|
||||||
depth: 0,
|
|
||||||
sourcePort: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (browserGatewayUiUrl) {
|
||||||
|
buildBrowserGatewayConfig({
|
||||||
|
config_output,
|
||||||
|
browserGatewayResourcesMap,
|
||||||
|
browserGatewayUiUrl,
|
||||||
|
maintenancePageUiUrl,
|
||||||
|
badgerMiddlewareName,
|
||||||
|
redirectHttpsMiddlewareName,
|
||||||
|
resolveTls: ({
|
||||||
|
fullDomain,
|
||||||
|
hasSubdomain,
|
||||||
|
domainCertResolver,
|
||||||
|
preferWildcardCert
|
||||||
|
}) =>
|
||||||
|
buildWildcardTls({
|
||||||
|
fullDomain,
|
||||||
|
hasSubdomain,
|
||||||
|
domainCertResolver,
|
||||||
|
preferWildcardCert
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that
|
||||||
|
// Traefik generates TLS certificates for those domains even when no
|
||||||
|
// matching resource exists yet.
|
||||||
|
if (siteResourcesWithFullDomain.length > 0) {
|
||||||
|
// Build a set of domains already covered by normal resources
|
||||||
|
const existingFullDomains = new Set<string>();
|
||||||
|
for (const resource of resourcesMap.values()) {
|
||||||
|
if (resource.fullDomain) {
|
||||||
|
existingFullDomains.add(resource.fullDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSiteResourceAliasCertPlaceholders({
|
||||||
|
config_output,
|
||||||
|
siteResourcesWithFullDomain,
|
||||||
|
existingFullDomains,
|
||||||
|
maintenancePageUiUrl,
|
||||||
|
redirectHttpsMiddlewareName,
|
||||||
|
resolveTls: (fullDomain) =>
|
||||||
|
buildWildcardTls({
|
||||||
|
fullDomain,
|
||||||
|
hasSubdomain: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aiGatewayUrl) {
|
||||||
|
// The AI gateway may live on a different host than the inference
|
||||||
|
// resource itself (e.g. a remote exit node forwarding to the
|
||||||
|
// central dashboard over a tunnel). passHostHeader would forward
|
||||||
|
// the resource's own Host, which that external host won't
|
||||||
|
// recognize, so we pin the Host header to the gateway's own host
|
||||||
|
// and smuggle the original resource host through in "p-host"
|
||||||
|
// instead.
|
||||||
|
const aiGatewayHost = getAiGatewayHost(aiGatewayUrl);
|
||||||
|
|
||||||
|
if (!config_output.http.middlewares) {
|
||||||
|
config_output.http.middlewares = {};
|
||||||
|
}
|
||||||
|
Object.assign(
|
||||||
|
config_output.http.middlewares,
|
||||||
|
buildAiGatewayTrustMiddlewares()
|
||||||
|
);
|
||||||
|
|
||||||
|
const aiGatewayClientIpMiddleware = buildAiGatewayClientIpMiddleware();
|
||||||
|
const enableAiGatewayClientIpHeader = !!aiGatewayClientIpMiddleware;
|
||||||
|
if (aiGatewayClientIpMiddleware) {
|
||||||
|
Object.assign(
|
||||||
|
config_output.http.middlewares,
|
||||||
|
aiGatewayClientIpMiddleware
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public inference resources: same TLS/cert-resolver handling as
|
||||||
|
// plain http-mode resources, but the service points at the AI
|
||||||
|
// gateway instead of any real backend targets.
|
||||||
|
//
|
||||||
|
// Inference-mode resources are allowed to share a fullDomain with
|
||||||
|
// each other (see createResource.ts), and a siteResource inference
|
||||||
|
// alias can share that domain too - all of them proxy to the same
|
||||||
|
// aiGatewayUrl, so dedupe by fullDomain here (lowest resourceId
|
||||||
|
// wins, for stable output across regenerations) and skip the
|
||||||
|
// siteResource alias router for any domain already covered below.
|
||||||
|
const eligibleInferenceResources = inferenceResources
|
||||||
|
.filter((ir) => ir.enabled && ir.domainId && ir.fullDomain)
|
||||||
|
.sort((a, b) => a.resourceId - b.resourceId);
|
||||||
|
const dedupedInferenceResources = new Map<
|
||||||
|
string,
|
||||||
|
(typeof eligibleInferenceResources)[number]
|
||||||
|
>();
|
||||||
|
for (const ir of eligibleInferenceResources) {
|
||||||
|
if (!dedupedInferenceResources.has(ir.fullDomain!)) {
|
||||||
|
dedupedInferenceResources.set(ir.fullDomain!, ir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicInferenceDomains = new Set<string>();
|
||||||
|
for (const ir of dedupedInferenceResources.values()) {
|
||||||
|
if (!config_output.http.routers) config_output.http.routers = {};
|
||||||
|
if (!config_output.http.services) config_output.http.services = {};
|
||||||
|
|
||||||
|
const fullDomain = ir.fullDomain!;
|
||||||
|
const irKey = `inference-r${ir.resourceId}`;
|
||||||
|
const routerName = `${irKey}-router`;
|
||||||
|
const serviceName = `${irKey}-service`;
|
||||||
|
|
||||||
|
const rule = buildHostRule(fullDomain, ir.wildcard);
|
||||||
|
|
||||||
|
const tls = buildWildcardTls({
|
||||||
|
fullDomain,
|
||||||
|
hasSubdomain: !!ir.subdomain,
|
||||||
|
domainCertResolver: ir.domainCertResolver,
|
||||||
|
preferWildcardCert: ir.preferWildcardCert
|
||||||
|
});
|
||||||
|
|
||||||
|
const irHeadersMiddlewareName = `${irKey}-headers-middleware`;
|
||||||
|
config_output.http.middlewares[irHeadersMiddlewareName] =
|
||||||
|
buildAiGatewayHostHeaderMiddleware(aiGatewayHost, fullDomain);
|
||||||
|
|
||||||
|
const additionalMiddlewares =
|
||||||
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
|
const routerMiddlewares = [
|
||||||
|
badgerMiddlewareName,
|
||||||
|
AI_GATEWAY_TRUST_MIDDLEWARE_RESOURCE,
|
||||||
|
irHeadersMiddlewareName,
|
||||||
|
...additionalMiddlewares
|
||||||
|
];
|
||||||
|
|
||||||
|
const { routers, services } = buildAiGatewayRouterAndService({
|
||||||
|
routerName,
|
||||||
|
serviceName,
|
||||||
|
rule,
|
||||||
|
ssl: ir.ssl,
|
||||||
|
tls,
|
||||||
|
priority: 100,
|
||||||
|
routerMiddlewares,
|
||||||
|
aiGatewayUrl,
|
||||||
|
redirectHttpsMiddlewareName
|
||||||
|
});
|
||||||
|
Object.assign(config_output.http.routers, routers);
|
||||||
|
Object.assign(config_output.http.services, services);
|
||||||
|
publicInferenceDomains.add(fullDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private (siteResource) inference resources: routed by their alias
|
||||||
|
// instead of a public fullDomain, and deliberately WITHOUT the
|
||||||
|
// badger middleware - no per-user auth/policy stack exists for
|
||||||
|
// siteResources today, so gating here is reachability-only for now.
|
||||||
|
const siteResourcesInference = await db
|
||||||
|
.selectDistinct({
|
||||||
|
siteResourceId: siteResources.siteResourceId,
|
||||||
|
fullDomain: siteResources.fullDomain,
|
||||||
|
ssl: siteResources.ssl,
|
||||||
|
enabled: siteResources.enabled
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.mode, "inference"),
|
||||||
|
eq(siteResources.enabled, true),
|
||||||
|
isNotNull(siteResources.fullDomain)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exitNode) {
|
||||||
|
for (const sr of siteResourcesInference) {
|
||||||
|
if (!sr.enabled || !sr.fullDomain) continue;
|
||||||
|
|
||||||
|
// A public inference resource already owns a router for
|
||||||
|
// this exact fullDomain - both point at the same AI gateway,
|
||||||
|
// so avoid registering a duplicate router for it here.
|
||||||
|
if (publicInferenceDomains.has(sr.fullDomain)) continue;
|
||||||
|
|
||||||
|
if (!config_output.http.routers)
|
||||||
|
config_output.http.routers = {};
|
||||||
|
if (!config_output.http.services)
|
||||||
|
config_output.http.services = {};
|
||||||
|
|
||||||
|
const fullDomain = sr.fullDomain;
|
||||||
|
const srKey = `inference-sr${sr.siteResourceId}`;
|
||||||
|
const routerName = `${srKey}-router`;
|
||||||
|
const serviceName = `${srKey}-service`;
|
||||||
|
const rule = `Host(\`${fullDomain}\`) && ClientIP(\`${exitNode.address}\`)`; // restrict to coming from the exit node ip range that the client is connected to
|
||||||
|
|
||||||
|
// siteResource aliases don't have a per-domain cert resolver
|
||||||
|
// stored, so always fall back to the global defaults.
|
||||||
|
const tls = buildWildcardTls({
|
||||||
|
fullDomain,
|
||||||
|
hasSubdomain: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const srHeadersMiddlewareName = `${srKey}-headers-middleware`;
|
||||||
|
if (!config_output.http.middlewares) {
|
||||||
|
config_output.http.middlewares = {};
|
||||||
|
}
|
||||||
|
config_output.http.middlewares[srHeadersMiddlewareName] =
|
||||||
|
buildAiGatewayHostHeaderMiddleware(
|
||||||
|
aiGatewayHost,
|
||||||
|
fullDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
const additionalMiddlewares =
|
||||||
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
|
const routerMiddlewares = [
|
||||||
|
...(enableAiGatewayClientIpHeader
|
||||||
|
? [AI_GATEWAY_CLIENT_IP_MIDDLEWARE_NAME]
|
||||||
|
: []),
|
||||||
|
AI_GATEWAY_TRUST_MIDDLEWARE_SITE_RESOURCE,
|
||||||
|
srHeadersMiddlewareName,
|
||||||
|
...additionalMiddlewares
|
||||||
|
];
|
||||||
|
|
||||||
|
const { routers, services } = buildAiGatewayRouterAndService({
|
||||||
|
routerName,
|
||||||
|
serviceName,
|
||||||
|
rule,
|
||||||
|
ssl: sr.ssl,
|
||||||
|
tls,
|
||||||
|
priority: 200, // we want to match on the site resource first because the clientIP rule is more specific than the public inference resource rule, which is just the exit node IP range. so we give it a higher priority to ensure it matches first.
|
||||||
|
routerMiddlewares,
|
||||||
|
aiGatewayUrl,
|
||||||
|
redirectHttpsMiddlewareName
|
||||||
|
});
|
||||||
|
Object.assign(config_output.http.routers, routers);
|
||||||
|
Object.assign(config_output.http.services, services);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config_output;
|
return config_output;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the customRequestHeaders middleware definition for a resource's
|
||||||
|
* custom headers + setHostHeader config. Returns null when there are no
|
||||||
|
* headers to set, so the caller can skip attaching the middleware.
|
||||||
|
*/
|
||||||
|
export function buildCustomHeadersMiddleware(
|
||||||
|
headers: string | null | undefined,
|
||||||
|
setHostHeader: string | null | undefined,
|
||||||
|
resourceId: number
|
||||||
|
): { headers: { customRequestHeaders: { [key: string]: string } } } | null {
|
||||||
|
const headersObj: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
if (headers) {
|
||||||
|
let headersArr: { name: string; value: string }[] = [];
|
||||||
|
try {
|
||||||
|
headersArr = JSON.parse(headers) as {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to parse headers for resource ${resourceId}: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
headersArr.forEach((header) => {
|
||||||
|
headersObj[header.name] = header.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setHostHeader) {
|
||||||
|
headersObj["Host"] = setHostHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(headersObj).length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: headersObj
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { TargetWithSite } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the loadBalancer.servers list for an HTTP-mode resource, preferring
|
||||||
|
* currently-online sites but falling back to all enabled/healthy targets if
|
||||||
|
* none are online yet (so there's still some feedback before sites report
|
||||||
|
* back over the receive-bandwidth endpoint).
|
||||||
|
*/
|
||||||
|
export function buildHttpLoadBalancerServers(targets: TargetWithSite[]) {
|
||||||
|
const anySitesOnline = targets.some((target) => target.site.online);
|
||||||
|
|
||||||
|
return targets
|
||||||
|
.filter((target) => {
|
||||||
|
if (!target.enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.health == "unhealthy") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any sites are online, exclude offline sites
|
||||||
|
if (anySitesOnline && !target.site.online) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
target.site.type === "local" ||
|
||||||
|
target.site.type === "wireguard"
|
||||||
|
) {
|
||||||
|
if (!target.ip || !target.port || !target.method) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (target.site.type === "newt") {
|
||||||
|
if (
|
||||||
|
!target.internalPort ||
|
||||||
|
!target.method ||
|
||||||
|
!target.site.subnet
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((target) => {
|
||||||
|
if (
|
||||||
|
target.site.type === "local" ||
|
||||||
|
target.site.type === "wireguard"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
url: `${target.method}://${target.ip}:${target.port}`
|
||||||
|
};
|
||||||
|
} else if (target.site.type === "newt") {
|
||||||
|
const ip = target.site.subnet!.split("/")[0];
|
||||||
|
return {
|
||||||
|
url: `${target.method}://${ip}:${target.internalPort}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(v, i, a) => a.findIndex((t) => t && v && t.url === v.url) === i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStickySessionCookie(ssl: boolean | null) {
|
||||||
|
return {
|
||||||
|
sticky: {
|
||||||
|
cookie: {
|
||||||
|
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
||||||
|
secure: ssl,
|
||||||
|
httpOnly: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the loadBalancer.servers list for a TCP/UDP-mode resource.
|
||||||
|
*/
|
||||||
|
export function buildTcpUdpLoadBalancerServers(targets: TargetWithSite[]) {
|
||||||
|
const anySitesOnline = targets.some((target) => target.site.online);
|
||||||
|
|
||||||
|
return targets
|
||||||
|
.filter((target) => {
|
||||||
|
if (!target.enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any sites are online, exclude offline sites
|
||||||
|
if (anySitesOnline && !target.site.online) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
target.site.type === "local" ||
|
||||||
|
target.site.type === "wireguard"
|
||||||
|
) {
|
||||||
|
if (!target.ip || !target.port) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (target.site.type === "newt") {
|
||||||
|
if (!target.internalPort || !target.site.subnet) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((target) => {
|
||||||
|
if (
|
||||||
|
target.site.type === "local" ||
|
||||||
|
target.site.type === "wireguard"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
address: `${target.ip}:${target.port}`
|
||||||
|
};
|
||||||
|
} else if (target.site.type === "newt") {
|
||||||
|
const ip = target.site.subnet!.split("/")[0];
|
||||||
|
return {
|
||||||
|
address: `${ip}:${target.internalPort}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildStickySessionIp() {
|
||||||
|
return {
|
||||||
|
sticky: {
|
||||||
|
ipStrategy: {
|
||||||
|
depth: 0,
|
||||||
|
sourcePort: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,64 @@
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create (if configured) and attach a path-rewrite middleware for a
|
||||||
|
* resource, mutating both config_output.http.middlewares and the
|
||||||
|
* router's middleware chain. Shared by the OSS and private Traefik config
|
||||||
|
* generators, which apply it identically.
|
||||||
|
*/
|
||||||
|
export function applyPathRewriteMiddleware(
|
||||||
|
config_output: any,
|
||||||
|
resourceId: number,
|
||||||
|
key: string,
|
||||||
|
path: string | null,
|
||||||
|
pathMatchType: string | null,
|
||||||
|
rewritePath: string | null,
|
||||||
|
rewritePathType: string | null,
|
||||||
|
routerMiddlewares: string[]
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
rewritePath === null ||
|
||||||
|
path === null ||
|
||||||
|
!pathMatchType ||
|
||||||
|
!rewritePathType
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rewriteMiddlewareName = `rewrite-r${resourceId}-${key}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rewriteResult = createPathRewriteMiddleware(
|
||||||
|
rewriteMiddlewareName,
|
||||||
|
path,
|
||||||
|
pathMatchType,
|
||||||
|
rewritePath,
|
||||||
|
rewritePathType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!config_output.http.middlewares) {
|
||||||
|
config_output.http.middlewares = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
config_output.http.middlewares,
|
||||||
|
rewriteResult.middlewares
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rewriteResult.chain) {
|
||||||
|
// For chained middlewares (like stripPrefix + addPrefix)
|
||||||
|
routerMiddlewares.push(...rewriteResult.chain);
|
||||||
|
} else {
|
||||||
|
// Single middleware
|
||||||
|
routerMiddlewares.push(rewriteMiddlewareName);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to create path rewrite middleware for resource ${resourceId}: ${error}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function createPathRewriteMiddleware(
|
export default function createPathRewriteMiddleware(
|
||||||
middlewareName: string,
|
middlewareName: string,
|
||||||
path: string,
|
path: string,
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Build the Host()/HostRegexp() Traefik rule for a resource's domain.
|
||||||
|
* Wildcard resources match any single subdomain via HostRegexp.
|
||||||
|
*/
|
||||||
|
export function buildHostRule(
|
||||||
|
fullDomain: string,
|
||||||
|
wildcard?: boolean | null
|
||||||
|
): string {
|
||||||
|
if (wildcard && fullDomain.startsWith("*.")) {
|
||||||
|
// Convert *.foo.bar.com -> HostRegexp(`^[^.]+\.foo\.bar\.com$`)
|
||||||
|
const escaped = fullDomain.slice(2).replace(/\./g, "\\.");
|
||||||
|
return `HostRegexp(\`^[^.]+\\.${escaped}$\`)`;
|
||||||
|
}
|
||||||
|
return `Host(\`${fullDomain}\`)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a path-matching clause to a Traefik rule based on the resource's
|
||||||
|
* configured path and pathMatchType.
|
||||||
|
*/
|
||||||
|
export function appendPathMatch(
|
||||||
|
rule: string,
|
||||||
|
path: string | null | undefined,
|
||||||
|
pathMatchType: string | null | undefined
|
||||||
|
): string {
|
||||||
|
if (!path || !pathMatchType) return rule;
|
||||||
|
|
||||||
|
let p = path;
|
||||||
|
if (!p.startsWith("/")) {
|
||||||
|
p = `/${p}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathMatchType === "exact") {
|
||||||
|
return `${rule} && Path(\`${p}\`)`;
|
||||||
|
} else if (pathMatchType === "prefix") {
|
||||||
|
return `${rule} && PathPrefix(\`${p}\`)`;
|
||||||
|
} else if (pathMatchType === "regex") {
|
||||||
|
return `${rule} && PathRegexp(\`${path}\`)`; // this is the raw path because it's a regex
|
||||||
|
}
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the router priority for a resource, favoring an explicit override
|
||||||
|
* and otherwise deriving it from the path match specificity.
|
||||||
|
*/
|
||||||
|
export function computeRoutePriority(
|
||||||
|
priority: number | null | undefined,
|
||||||
|
path: string | null | undefined,
|
||||||
|
pathMatchType: string | null | undefined
|
||||||
|
): number {
|
||||||
|
if (priority && priority != 100) {
|
||||||
|
return priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = 100;
|
||||||
|
if (path && pathMatchType) {
|
||||||
|
p += 10;
|
||||||
|
if (pathMatchType === "exact") {
|
||||||
|
p += 5;
|
||||||
|
} else if (pathMatchType === "prefix") {
|
||||||
|
p += 3;
|
||||||
|
} else if (pathMatchType === "regex") {
|
||||||
|
p += 2;
|
||||||
|
}
|
||||||
|
if (path === "/") {
|
||||||
|
p = 1; // lowest for catch-all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
|
export type SiteResourceAliasRow = {
|
||||||
|
siteResourceId: number;
|
||||||
|
fullDomain: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add placeholder Traefik routes for siteResource HTTP aliases so Traefik
|
||||||
|
* generates TLS certificates for those domains even before a matching
|
||||||
|
* resource exists. Requests that land on these routes before a real
|
||||||
|
* resource is created are served the placeholder page. TLS/cert-resolver
|
||||||
|
* handling differs between the OSS and private (pangolin-dns aware) config
|
||||||
|
* generators, so callers resolve that themselves via resolveTls - returning
|
||||||
|
* null skips the alias (no valid cert available yet).
|
||||||
|
*/
|
||||||
|
export function buildSiteResourceAliasCertPlaceholders(params: {
|
||||||
|
config_output: any;
|
||||||
|
siteResourcesWithFullDomain: SiteResourceAliasRow[];
|
||||||
|
existingFullDomains: Set<string>;
|
||||||
|
maintenancePageUiUrl: string | null;
|
||||||
|
redirectHttpsMiddlewareName: string;
|
||||||
|
resolveTls: (fullDomain: string) => any | null;
|
||||||
|
}): void {
|
||||||
|
const {
|
||||||
|
config_output,
|
||||||
|
siteResourcesWithFullDomain,
|
||||||
|
existingFullDomains,
|
||||||
|
maintenancePageUiUrl,
|
||||||
|
redirectHttpsMiddlewareName,
|
||||||
|
resolveTls
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
if (siteResourcesWithFullDomain.length === 0 || !maintenancePageUiUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sr of siteResourcesWithFullDomain) {
|
||||||
|
if (!sr.fullDomain) continue;
|
||||||
|
|
||||||
|
// Skip if this alias is already handled by a resource router
|
||||||
|
if (existingFullDomains.has(sr.fullDomain)) continue;
|
||||||
|
|
||||||
|
const fullDomain = sr.fullDomain;
|
||||||
|
const srKey = `site-resource-cert-${sr.siteResourceId}`;
|
||||||
|
const siteResourceServiceName = `${srKey}-service`;
|
||||||
|
const siteResourceRouterName = `${srKey}-router`;
|
||||||
|
const siteResourceRewriteMiddlewareName = `${srKey}-rewrite`;
|
||||||
|
|
||||||
|
if (!config_output.http.routers) {
|
||||||
|
config_output.http.routers = {};
|
||||||
|
}
|
||||||
|
if (!config_output.http.services) {
|
||||||
|
config_output.http.services = {};
|
||||||
|
}
|
||||||
|
if (!config_output.http.middlewares) {
|
||||||
|
config_output.http.middlewares = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service pointing at the internal maintenance/Next.js page
|
||||||
|
config_output.http.services[siteResourceServiceName] = {
|
||||||
|
loadBalancer: {
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: maintenancePageUiUrl
|
||||||
|
}
|
||||||
|
],
|
||||||
|
passHostHeader: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Middleware that rewrites any path to /private-maintenance-screen
|
||||||
|
config_output.http.middlewares[siteResourceRewriteMiddlewareName] = {
|
||||||
|
replacePathRegex: {
|
||||||
|
regex: "^/(.*)",
|
||||||
|
replacement: "/private-maintenance-screen"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// HTTP -> HTTPS redirect so the ACME challenge can be served
|
||||||
|
config_output.http.routers[`${siteResourceRouterName}-redirect`] = {
|
||||||
|
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
|
||||||
|
middlewares: [redirectHttpsMiddlewareName],
|
||||||
|
service: siteResourceServiceName,
|
||||||
|
rule: `Host(\`${fullDomain}\`)`,
|
||||||
|
priority: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine TLS / cert-resolver configuration
|
||||||
|
const tls = resolveTls(fullDomain);
|
||||||
|
if (tls === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPS router - presence of this entry triggers cert generation
|
||||||
|
config_output.http.routers[siteResourceRouterName] = {
|
||||||
|
entryPoints: [config.getRawConfig().traefik.https_entrypoint],
|
||||||
|
service: siteResourceServiceName,
|
||||||
|
middlewares: [siteResourceRewriteMiddlewareName],
|
||||||
|
rule: `Host(\`${fullDomain}\`)`,
|
||||||
|
priority: 100,
|
||||||
|
tls
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assets bypass router - lets Next.js static files load without rewrite
|
||||||
|
config_output.http.routers[`${siteResourceRouterName}-assets`] = {
|
||||||
|
entryPoints: [config.getRawConfig().traefik.https_entrypoint],
|
||||||
|
service: siteResourceServiceName,
|
||||||
|
rule: `Host(\`${fullDomain}\`) && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
|
||||||
|
priority: 101,
|
||||||
|
tls
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Target } from "@server/db";
|
||||||
|
|
||||||
|
// Extended target type with site information, shared between the OSS and
|
||||||
|
// private getTraefikConfig implementations.
|
||||||
|
export type TargetWithSite = Target & {
|
||||||
|
resourceId: number;
|
||||||
|
targetId: number;
|
||||||
|
ip: string | null;
|
||||||
|
method: string | null;
|
||||||
|
port: number | null;
|
||||||
|
internalPort: number | null;
|
||||||
|
enabled: boolean;
|
||||||
|
health: string | null;
|
||||||
|
site: {
|
||||||
|
siteId: number;
|
||||||
|
type: string;
|
||||||
|
subnet: string | null;
|
||||||
|
exitNodeId: number | null;
|
||||||
|
online: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import {
|
||||||
|
generateId,
|
||||||
|
generateIdFromEntropySize
|
||||||
|
} from "@server/auth/sessions/app";
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
resources,
|
||||||
|
User,
|
||||||
|
virtualApiKeyResources,
|
||||||
|
virtualApiKeys,
|
||||||
|
type Transaction,
|
||||||
|
type VirtualApiKey
|
||||||
|
} from "@server/db";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import { decrypt, encrypt } from "@server/lib/crypto";
|
||||||
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
|
|
||||||
|
export {
|
||||||
|
VIRTUAL_API_KEY_PREFIX,
|
||||||
|
formatVirtualApiKeyCredential,
|
||||||
|
formatVirtualApiKeyPreview,
|
||||||
|
looksLikeVirtualApiKeyCredential,
|
||||||
|
stripVirtualApiKeyAuthHeaders
|
||||||
|
} from "@app/lib/virtualApiKeyFormat";
|
||||||
|
|
||||||
|
export type MintedVirtualApiKeySecret = {
|
||||||
|
virtualApiKeyId: string;
|
||||||
|
secret: string;
|
||||||
|
lastChars: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PublicVirtualApiKey = Omit<VirtualApiKey, "token"> & {
|
||||||
|
secret?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mintVirtualApiKeySecret(): MintedVirtualApiKeySecret {
|
||||||
|
const secret = generateIdFromEntropySize(16);
|
||||||
|
return {
|
||||||
|
virtualApiKeyId: generateId(8),
|
||||||
|
secret,
|
||||||
|
lastChars: secret.slice(-4)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptVirtualApiKeyToken(secret: string): string {
|
||||||
|
return encrypt(secret, config.getRawConfig().server.secret!);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decryptVirtualApiKeyToken(ciphertext: string): string {
|
||||||
|
return decrypt(ciphertext, config.getRawConfig().server.secret!);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toPublicVirtualApiKey(
|
||||||
|
row: VirtualApiKey,
|
||||||
|
options?: { includeSecret?: boolean }
|
||||||
|
): PublicVirtualApiKey {
|
||||||
|
const { token, ...rest } = row;
|
||||||
|
if (!options?.includeSecret) {
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
secret: decryptVirtualApiKeyToken(token)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function assertManualKeyResourcesInOrg(params: {
|
||||||
|
allResources: boolean;
|
||||||
|
resourceIds: number[];
|
||||||
|
orgId: string;
|
||||||
|
}): Promise<{ ok: true } | { ok: false; message: string }> {
|
||||||
|
const { allResources, resourceIds, orgId } = params;
|
||||||
|
|
||||||
|
if (allResources) {
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceIds.length === 0) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"Select at least one public inference resource, or enable all public inference resources"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueIds = [...new Set(resourceIds)];
|
||||||
|
const rows = await db
|
||||||
|
.select({ resourceId: resources.resourceId })
|
||||||
|
.from(resources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(resources.orgId, orgId),
|
||||||
|
eq(resources.mode, "inference"),
|
||||||
|
inArray(resources.resourceId, uniqueIds)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length !== uniqueIds.length) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message:
|
||||||
|
"One or more resources are invalid public inference resources for this organization"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function replaceVirtualApiKeyResources(
|
||||||
|
trx: Transaction | typeof db,
|
||||||
|
virtualApiKeyId: string,
|
||||||
|
resourceIds: number[]
|
||||||
|
): Promise<void> {
|
||||||
|
await trx
|
||||||
|
.delete(virtualApiKeyResources)
|
||||||
|
.where(eq(virtualApiKeyResources.virtualApiKeyId, virtualApiKeyId));
|
||||||
|
|
||||||
|
const uniqueIds = [...new Set(resourceIds)];
|
||||||
|
if (uniqueIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.insert(virtualApiKeyResources).values(
|
||||||
|
uniqueIds.map((resourceId) => ({
|
||||||
|
virtualApiKeyId,
|
||||||
|
resourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectUserVirtualApiKey(
|
||||||
|
orgId: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<VirtualApiKey | null> {
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(virtualApiKeys)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(virtualApiKeys.orgId, orgId),
|
||||||
|
eq(virtualApiKeys.userId, userId),
|
||||||
|
eq(virtualApiKeys.kind, "user")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return existing ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrCreateUserVirtualApiKey(params: {
|
||||||
|
orgId: string;
|
||||||
|
user: User;
|
||||||
|
createdByUserId?: string | null;
|
||||||
|
}): Promise<{ key: VirtualApiKey; secret: string }> {
|
||||||
|
const { orgId, user, createdByUserId } = params;
|
||||||
|
|
||||||
|
const existing = await selectUserVirtualApiKey(orgId, user.userId);
|
||||||
|
if (existing) {
|
||||||
|
return {
|
||||||
|
key: existing,
|
||||||
|
secret: decryptVirtualApiKeyToken(existing.token)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const minted = mintVirtualApiKeySecret();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [created] = await db
|
||||||
|
.insert(virtualApiKeys)
|
||||||
|
.values({
|
||||||
|
virtualApiKeyId: minted.virtualApiKeyId,
|
||||||
|
orgId,
|
||||||
|
kind: "user",
|
||||||
|
userId: user.userId,
|
||||||
|
name: `${user.name ?? user.username}'s API Key`,
|
||||||
|
description: null,
|
||||||
|
token: encryptVirtualApiKeyToken(minted.secret),
|
||||||
|
lastChars: minted.lastChars,
|
||||||
|
allResources: false,
|
||||||
|
expiresAt: null,
|
||||||
|
lastUsedAt: null,
|
||||||
|
createdAt: now,
|
||||||
|
createdByUserId: createdByUserId ?? null
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return { key: created, secret: minted.secret };
|
||||||
|
} catch {
|
||||||
|
const raced = await selectUserVirtualApiKey(orgId, user.userId);
|
||||||
|
if (raced) {
|
||||||
|
return {
|
||||||
|
key: raced,
|
||||||
|
secret: decryptVirtualApiKeyToken(raced.token)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error("Failed to create user virtual API key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rotateUserVirtualApiKey(params: {
|
||||||
|
orgId: string;
|
||||||
|
user: User;
|
||||||
|
createdByUserId?: string | null;
|
||||||
|
}): Promise<{ key: VirtualApiKey; secret: string }> {
|
||||||
|
const { orgId, user, createdByUserId } = params;
|
||||||
|
const existing = await selectUserVirtualApiKey(orgId, user.userId);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return getOrCreateUserVirtualApiKey(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
const minted = mintVirtualApiKeySecret();
|
||||||
|
const [updated] = await db
|
||||||
|
.update(virtualApiKeys)
|
||||||
|
.set({
|
||||||
|
token: encryptVirtualApiKeyToken(minted.secret),
|
||||||
|
lastChars: minted.lastChars,
|
||||||
|
createdByUserId:
|
||||||
|
createdByUserId !== undefined
|
||||||
|
? createdByUserId
|
||||||
|
: existing.createdByUserId
|
||||||
|
})
|
||||||
|
.where(eq(virtualApiKeys.virtualApiKeyId, existing.virtualApiKeyId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return { key: updated, secret: minted.secret };
|
||||||
|
}
|
||||||
@@ -27,6 +27,10 @@ export * from "./verifyUserHasAction";
|
|||||||
export * from "./verifyApiKeyAccess";
|
export * from "./verifyApiKeyAccess";
|
||||||
export * from "./verifySiteProvisioningKeyAccess";
|
export * from "./verifySiteProvisioningKeyAccess";
|
||||||
export * from "./verifyDomainAccess";
|
export * from "./verifyDomainAccess";
|
||||||
|
export * from "./verifyAiProviderAccess";
|
||||||
|
export * from "./verifyAiModelAccess";
|
||||||
|
export * from "./verifyAiBudgetAccess";
|
||||||
|
export * from "./verifyVirtualApiKeyAccess";
|
||||||
export * from "./verifyUserIsOrgOwner";
|
export * from "./verifyUserIsOrgOwner";
|
||||||
export * from "./verifyUserFromResourceSession";
|
export * from "./verifyUserFromResourceSession";
|
||||||
export * from "./verifySiteResourceAccess";
|
export * from "./verifySiteResourceAccess";
|
||||||
@@ -34,3 +38,4 @@ export * from "./logActionAudit";
|
|||||||
export * from "./verifyOlmAccess";
|
export * from "./verifyOlmAccess";
|
||||||
export * from "./verifyLimits";
|
export * from "./verifyLimits";
|
||||||
export * from "./verifyResourcePolicyAccess";
|
export * from "./verifyResourcePolicyAccess";
|
||||||
|
export * from "./verifyCertificateAccess";
|
||||||
|
|||||||
@@ -16,5 +16,9 @@ export * from "./verifyApiKeyClientAccess";
|
|||||||
export * from "./verifyApiKeySiteResourceAccess";
|
export * from "./verifyApiKeySiteResourceAccess";
|
||||||
export * from "./verifyApiKeyIdpAccess";
|
export * from "./verifyApiKeyIdpAccess";
|
||||||
export * from "./verifyApiKeyDomainAccess";
|
export * from "./verifyApiKeyDomainAccess";
|
||||||
|
export * from "./verifyApiKeyAiProviderAccess";
|
||||||
|
export * from "./verifyApiKeyAiModelAccess";
|
||||||
|
export * from "./verifyApiKeyAiBudgetAccess";
|
||||||
export * from "./verifyApiKeyResourcePolicyAccess";
|
export * from "./verifyApiKeyResourcePolicyAccess";
|
||||||
export * from "./verifyApiKeySiteProvisioningKeyAccess";
|
export * from "./verifyApiKeySiteProvisioningKeyAccess";
|
||||||
|
export * from "./verifyVirtualApiKeyAccess";
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { aiBudgets, apiKeyOrg, db } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyApiKeyAiBudgetAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const budgetIdRaw = getFirstString(req.params.budgetId);
|
||||||
|
const budgetId = Number.parseInt(budgetIdRaw ?? "", 10);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(budgetId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid budget ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [budget] = await db
|
||||||
|
.select()
|
||||||
|
.from(aiBudgets)
|
||||||
|
.where(eq(aiBudgets.budgetId, budgetId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!budget) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI budget with ID ${budgetId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
req.aiBudget = budget;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = budget.orgId;
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg || req.apiKeyOrg.orgId !== orgId) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.aiBudget = budget;
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying AI budget access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { aiModels, aiProviders, apiKeyOrg, db } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyApiKeyAiModelAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const modelIdRaw = getFirstString(req.params.modelId);
|
||||||
|
const modelId = Number.parseInt(modelIdRaw ?? "", 10);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(modelId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid model ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({
|
||||||
|
model: aiModels,
|
||||||
|
provider: aiProviders
|
||||||
|
})
|
||||||
|
.from(aiModels)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(aiModels.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(eq(aiModels.modelId, modelId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI model with ID ${modelId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
req.aiProvider = row.provider;
|
||||||
|
req.aiModel = row.model;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = row.provider.orgId;
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg || req.apiKeyOrg.orgId !== orgId) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.aiProvider = row.provider;
|
||||||
|
req.aiModel = row.model;
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying AI model access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { AiProvider, aiProviders, apiKeyOrg, db } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyApiKeyAiProviderAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const providerIdRaw = getFirstString(req.params.providerId);
|
||||||
|
const niceId = getFirstString(req.params.niceId);
|
||||||
|
const orgIdParam = getFirstString(req.params.orgId);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let provider: AiProvider | undefined;
|
||||||
|
|
||||||
|
if (niceId && orgIdParam) {
|
||||||
|
const [providerRes] = await db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiProviders.niceId, niceId),
|
||||||
|
eq(aiProviders.orgId, orgIdParam)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
provider = providerRes;
|
||||||
|
} else {
|
||||||
|
const providerId = Number.parseInt(providerIdRaw ?? "", 10);
|
||||||
|
|
||||||
|
if (Number.isNaN(providerId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid provider ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [providerRes] = await db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(eq(aiProviders.providerId, providerId))
|
||||||
|
.limit(1);
|
||||||
|
provider = providerRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerIdRaw || niceId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
req.aiProvider = provider;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = provider.orgId;
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg || req.apiKeyOrg.orgId !== orgId) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.aiProvider = provider;
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying AI provider access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { resources, targets, apiKeyOrg } from "@server/db";
|
import { aiProviders, resources, targets, apiKeyOrg } from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -43,16 +43,23 @@ export async function verifyApiKeyTargetAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceId = target.resourceId;
|
const { resourceId, providerId } = target;
|
||||||
if (!resourceId) {
|
if ((!resourceId && !providerId) || (resourceId && providerId)) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
`Target with ID ${targetId} does not have a resource ID`
|
`Target with ID ${targetId} has invalid ownership`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any target
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let orgId: string;
|
||||||
|
if (resourceId) {
|
||||||
const [resource] = await db
|
const [resource] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
@@ -68,11 +75,6 @@ export async function verifyApiKeyTargetAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiKey.isRoot) {
|
|
||||||
// Root keys can access any key in any org
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resource.orgId) {
|
if (!resource.orgId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -82,6 +84,26 @@ export async function verifyApiKeyTargetAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orgId = resource.orgId;
|
||||||
|
} else {
|
||||||
|
const [provider] = await db
|
||||||
|
.select()
|
||||||
|
.from(aiProviders)
|
||||||
|
.where(eq(aiProviders.providerId, providerId!))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI provider with ID ${providerId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
orgId = provider.orgId;
|
||||||
|
}
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
if (!req.apiKeyOrg) {
|
||||||
const apiKeyOrgResult = await db
|
const apiKeyOrgResult = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -89,7 +111,7 @@ export async function verifyApiKeyTargetAccess(
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
eq(apiKeyOrg.orgId, resource.orgId)
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
@@ -98,7 +120,7 @@ export async function verifyApiKeyTargetAccess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
if (!req.apiKeyOrg || req.apiKeyOrg.orgId !== orgId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.FORBIDDEN,
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { apiKeyOrg, db, virtualApiKeys } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyApiKeyVirtualApiKeyAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const virtualApiKeyId = getFirstString(req.params.virtualApiKeyId);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!virtualApiKeyId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid virtual API key ID"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [key] = await db
|
||||||
|
.select()
|
||||||
|
.from(virtualApiKeys)
|
||||||
|
.where(eq(virtualApiKeys.virtualApiKeyId, virtualApiKeyId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!key || key.kind !== "manual") {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Virtual API key with ID ${virtualApiKeyId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
req.virtualApiKey = key;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = key.orgId;
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg || req.apiKeyOrg.orgId !== orgId) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.virtualApiKey = key;
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying virtual API key access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { aiBudgets, db, userOrgs } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyAiBudgetAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const budgetIdRaw = getFirstString(req.params.budgetId);
|
||||||
|
const budgetId = Number.parseInt(budgetIdRaw ?? "", 10);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(budgetId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid budget ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [budget] = await db
|
||||||
|
.select()
|
||||||
|
.from(aiBudgets)
|
||||||
|
.where(eq(aiBudgets.budgetId, budgetId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!budget) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI budget with ID ${budgetId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = budget.orgId;
|
||||||
|
|
||||||
|
if (!req.userOrg || req.userOrg.orgId !== orgId) {
|
||||||
|
const userOrgRole = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.userOrg = userOrgRole[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"" + (policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.userOrgId = orgId;
|
||||||
|
req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId);
|
||||||
|
req.aiBudget = budget;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying AI budget access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { aiModels, aiProviders, db, userOrgs } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getFirstString } from "@server/lib/requestParams";
|
||||||
|
|
||||||
|
export async function verifyAiModelAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const userId = req.user!.userId;
|
||||||
|
const modelIdRaw = getFirstString(req.params.modelId);
|
||||||
|
const modelId = Number.parseInt(modelIdRaw ?? "", 10);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(modelId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid model ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.select({
|
||||||
|
model: aiModels,
|
||||||
|
provider: aiProviders
|
||||||
|
})
|
||||||
|
.from(aiModels)
|
||||||
|
.innerJoin(
|
||||||
|
aiProviders,
|
||||||
|
eq(aiModels.providerId, aiProviders.providerId)
|
||||||
|
)
|
||||||
|
.where(eq(aiModels.modelId, modelId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`AI model with ID ${modelId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = row.provider.orgId;
|
||||||
|
|
||||||
|
if (!req.userOrg || req.userOrg.orgId !== orgId) {
|
||||||
|
const userOrgRole = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
req.userOrg = userOrgRole[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.userOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"User does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
|
||||||
|
const policyCheck = await checkOrgAccessPolicy({
|
||||||
|
orgId: req.userOrg.orgId,
|
||||||
|
userId,
|
||||||
|
session: req.session
|
||||||
|
});
|
||||||
|
req.orgPolicyAllowed = policyCheck.allowed;
|
||||||
|
if (!policyCheck.allowed || policyCheck.error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"" + (policyCheck.error || "Unknown error")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.userOrgId = orgId;
|
||||||
|
req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId);
|
||||||
|
req.aiProvider = row.provider;
|
||||||
|
req.aiModel = row.model;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying AI model access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user