feat(core): improve byok editing (#15427)
fix #14287 fix #15359 fix #15424 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Redesigned workspace AI provider settings with connection testing, storage options, model selection, capability management, ordering, and custom endpoints. * AI chat model choices now adapt to the selected workspace and conversation route. * Added support for image-based AI requests. * **Bug Fixes** * Improved handling of unavailable or outdated model selections. * App configuration updates now reject overlapping paths and load deterministically. * **Tests** * Expanded coverage for provider models, AI chat scoping, image requests, and configuration validation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -4753,9 +4753,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "llm_adapter"
|
name = "llm_adapter"
|
||||||
version = "0.2.14"
|
version = "0.2.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f4086072a8f69a2a119e187367844d542f133f1bdf96ebdf495b49b83cf4a05"
|
checksum = "5cfb4eab8636c2e3ac87a221630f6b458e1a7ffb32e3701576840871b98d1bc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
|
|||||||
@@ -135,9 +135,25 @@ fn estimated_message_bytes(message: &PromptMessageContract) -> usize {
|
|||||||
let mut size = MESSAGE_FRAMING_BYTES
|
let mut size = MESSAGE_FRAMING_BYTES
|
||||||
.saturating_add(message.role.len())
|
.saturating_add(message.role.len())
|
||||||
.saturating_add(message.content.len());
|
.saturating_add(message.content.len());
|
||||||
|
if let Some(attachments) = &message.attachments {
|
||||||
|
size = attachments.iter().fold(size, |size, attachment| {
|
||||||
|
size.saturating_add(
|
||||||
|
serde_json::to_vec(&attachment_metadata(attachment))
|
||||||
|
.map(|bytes| bytes.len())
|
||||||
|
.unwrap_or(usize::MAX),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
for value in [
|
for value in [
|
||||||
message.attachments.as_ref().map(serde_json::to_vec),
|
message.params.as_ref().map(|params| {
|
||||||
message.params.as_ref().map(serde_json::to_vec),
|
let mut metadata = params.clone();
|
||||||
|
if let Some(Value::Array(attachments)) = metadata.get_mut("attachments") {
|
||||||
|
for attachment in attachments {
|
||||||
|
*attachment = attachment_metadata(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serde_json::to_vec(&metadata)
|
||||||
|
}),
|
||||||
message.response_format.as_ref().map(serde_json::to_vec),
|
message.response_format.as_ref().map(serde_json::to_vec),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -148,6 +164,32 @@ fn estimated_message_bytes(message: &PromptMessageContract) -> usize {
|
|||||||
size
|
size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn attachment_metadata(attachment: &Value) -> Value {
|
||||||
|
if attachment.as_str().is_some_and(|value| value.starts_with("data:")) {
|
||||||
|
return Value::String("data:".to_string());
|
||||||
|
}
|
||||||
|
let Some(object) = attachment.as_object() else {
|
||||||
|
return attachment.clone();
|
||||||
|
};
|
||||||
|
let inline = matches!(object.get("kind").and_then(Value::as_str), Some("data" | "bytes"));
|
||||||
|
let metadata = object
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| {
|
||||||
|
let value = if inline && key == "data" {
|
||||||
|
Value::Null
|
||||||
|
} else if matches!(key.as_str(), "url" | "attachment")
|
||||||
|
&& value.as_str().is_some_and(|value| value.starts_with("data:"))
|
||||||
|
{
|
||||||
|
Value::String("data:".to_string())
|
||||||
|
} else {
|
||||||
|
value.clone()
|
||||||
|
};
|
||||||
|
(key.clone(), value)
|
||||||
|
})
|
||||||
|
.collect::<Map<_, _>>();
|
||||||
|
Value::Object(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
fn select_history_turns(
|
fn select_history_turns(
|
||||||
fixed_messages: &[PromptMessageContract],
|
fixed_messages: &[PromptMessageContract],
|
||||||
history: &[PromptMessageContract],
|
history: &[PromptMessageContract],
|
||||||
@@ -266,6 +308,64 @@ mod tests {
|
|||||||
let emoji = estimated_message_bytes(&message("user", "😀😀😀"));
|
let emoji = estimated_message_bytes(&message("user", "😀😀😀"));
|
||||||
assert!(ascii < cjk);
|
assert!(ascii < cjk);
|
||||||
assert!(cjk < emoji);
|
assert!(cjk < emoji);
|
||||||
|
|
||||||
|
let small = serde_json::from_value(json!({
|
||||||
|
"role": "user",
|
||||||
|
"content": "describe",
|
||||||
|
"attachments": [{ "kind": "bytes", "data": "aW1n", "mimeType": "image/png" }]
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let large = serde_json::from_value(json!({
|
||||||
|
"role": "user",
|
||||||
|
"content": "describe",
|
||||||
|
"attachments": [{ "kind": "bytes", "data": "aW1n".repeat(100_000), "mimeType": "image/png" }]
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(estimated_message_bytes(&small), estimated_message_bytes(&large));
|
||||||
|
|
||||||
|
let legacy_small = serde_json::from_value(json!({
|
||||||
|
"role": "user",
|
||||||
|
"content": "describe",
|
||||||
|
"attachments": ["data:image/png;base64,aW1n"]
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let legacy_large = serde_json::from_value(json!({
|
||||||
|
"role": "user",
|
||||||
|
"content": "describe",
|
||||||
|
"attachments": [format!("data:image/png;base64,{}", "aW1n".repeat(100_000))]
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
estimated_message_bytes(&legacy_small),
|
||||||
|
estimated_message_bytes(&legacy_large)
|
||||||
|
);
|
||||||
|
|
||||||
|
let params_small = serde_json::from_value(json!({
|
||||||
|
"role": "user",
|
||||||
|
"content": "describe",
|
||||||
|
"params": {
|
||||||
|
"attachments": [{
|
||||||
|
"attachment": "data:image/png;base64,aW1n",
|
||||||
|
"mimeType": "image/png"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let params_large = serde_json::from_value(json!({
|
||||||
|
"role": "user",
|
||||||
|
"content": "describe",
|
||||||
|
"params": {
|
||||||
|
"attachments": [{
|
||||||
|
"attachment": format!("data:image/png;base64,{}", "aW1n".repeat(100_000)),
|
||||||
|
"mimeType": "image/png"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
estimated_message_bytes(¶ms_small),
|
||||||
|
estimated_message_bytes(¶ms_large)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ mod ffi;
|
|||||||
mod prompt_catalog;
|
mod prompt_catalog;
|
||||||
pub(crate) mod route;
|
pub(crate) mod route;
|
||||||
|
|
||||||
|
pub use action::copilot_action_recipe;
|
||||||
|
pub use byok::{
|
||||||
|
ByokCapabilityInput, ByokCatalogModelOutput, ByokCatalogOutput, ByokCatalogProviderOutput, ByokEndpointInput,
|
||||||
|
ByokLocalLeaseOutput, ByokModelDeclarationInput, ByokModelProbeCheckOutput, ByokModelProbeOutput,
|
||||||
|
ByokProbeCheckInput, ByokProbeResultOutput, ByokProbeStatusOutput, ByokProfileDefinitionInput, ByokProfileOutput,
|
||||||
|
ByokValidationOutput, CreateByokLocalLeaseInput, CreateByokLocalLeaseProviderInput, CreateByokProfileInput,
|
||||||
|
ProbeByokDraftInput, ProbeByokProfileInput, ReorderByokProfilesInput, ReplaceByokProfileInput,
|
||||||
|
RotateByokCredentialInput, byok_catalog,
|
||||||
|
};
|
||||||
|
|
||||||
pub use self::core::{
|
pub use self::core::{
|
||||||
capability::llm_match_model_capabilities,
|
capability::llm_match_model_capabilities,
|
||||||
model_registry::{llm_match_model_registry, llm_resolve_model_registry_variant},
|
model_registry::{llm_match_model_registry, llm_resolve_model_registry_variant},
|
||||||
@@ -20,16 +30,6 @@ pub use self::core::{
|
|||||||
structured_output::{llm_canonical_json_schema_hash, llm_validate_json_schema},
|
structured_output::{llm_canonical_json_schema_hash, llm_validate_json_schema},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use action::copilot_action_recipe;
|
|
||||||
pub use byok::{
|
|
||||||
ByokCapabilityInput, ByokCatalogModelOutput, ByokCatalogOutput, ByokCatalogProviderOutput, ByokEndpointInput,
|
|
||||||
ByokLocalLeaseOutput, ByokModelDeclarationInput, ByokModelProbeCheckOutput, ByokModelProbeOutput,
|
|
||||||
ByokProbeCheckInput, ByokProbeResultOutput, ByokProbeStatusOutput, ByokProfileDefinitionInput, ByokProfileOutput,
|
|
||||||
ByokValidationOutput, CreateByokLocalLeaseInput, CreateByokLocalLeaseProviderInput, CreateByokProfileInput,
|
|
||||||
ProbeByokDraftInput, ProbeByokProfileInput, ReorderByokProfilesInput, ReplaceByokProfileInput,
|
|
||||||
RotateByokCredentialInput, byok_catalog,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[napi_derive::napi(catch_unwind)]
|
#[napi_derive::napi(catch_unwind)]
|
||||||
pub fn llm_get_byok_catalog() -> ByokCatalogOutput {
|
pub fn llm_get_byok_catalog() -> ByokCatalogOutput {
|
||||||
byok_catalog()
|
byok_catalog()
|
||||||
@@ -44,6 +44,8 @@ pub use route::{
|
|||||||
CopilotAccessProjection, CopilotExecuteInput, CopilotManagedTier, CopilotRouteCheckInput, CopilotTargetOverrideInput,
|
CopilotAccessProjection, CopilotExecuteInput, CopilotManagedTier, CopilotRouteCheckInput, CopilotTargetOverrideInput,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub(crate) use self::core::contracts::LlmImageRequestContract;
|
||||||
|
|
||||||
pub(crate) fn invalid_arg(message: impl Into<String>) -> napi::Error {
|
pub(crate) fn invalid_arg(message: impl Into<String>) -> napi::Error {
|
||||||
napi::Error::new(napi::Status::InvalidArg, message.into())
|
napi::Error::new(napi::Status::InvalidArg, message.into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,9 +432,10 @@ fn backend_error_kind(error: &BackendError) -> &'static str {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use llm_adapter::target::BackendEndpoint;
|
use llm_adapter::target::BackendEndpoint;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn connection_probe_errors_are_low_information() {
|
fn connection_probe_errors_are_low_information() {
|
||||||
let (url, headers) = probe_request("openai", &ByokEndpoint::ProviderDefault, "secret".to_string());
|
let (url, headers) = probe_request("openai", &ByokEndpoint::ProviderDefault, "secret".to_string());
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use zeroize::Zeroizing;
|
|||||||
use super::{COPILOT_REQUEST_TIMEOUT, RuntimeError, RuntimeResult, context};
|
use super::{COPILOT_REQUEST_TIMEOUT, RuntimeError, RuntimeResult, context};
|
||||||
use crate::{
|
use crate::{
|
||||||
llm::{
|
llm::{
|
||||||
|
LlmImageRequestContract,
|
||||||
byok::{ByokEndpoint, CredentialEnvelopeKey},
|
byok::{ByokEndpoint, CredentialEnvelopeKey},
|
||||||
route::{
|
route::{
|
||||||
AuthorizedProfileRef, AuthorizedTargetRef, CatalogSlot, CredentialRef, RouteOperation, with_request_requirements,
|
AuthorizedProfileRef, AuthorizedTargetRef, CatalogSlot, CredentialRef, RouteOperation, with_request_requirements,
|
||||||
@@ -83,7 +84,14 @@ pub(super) fn request_and_slot(
|
|||||||
}
|
}
|
||||||
RouteOperation::Embedding => ExecutableRequest::Embedding(parse_request(request)?),
|
RouteOperation::Embedding => ExecutableRequest::Embedding(parse_request(request)?),
|
||||||
RouteOperation::Rerank => ExecutableRequest::Rerank(parse_request(request)?),
|
RouteOperation::Rerank => ExecutableRequest::Rerank(parse_request(request)?),
|
||||||
RouteOperation::Image => ExecutableRequest::Image(Box::new(parse_request(request)?)),
|
RouteOperation::Image => {
|
||||||
|
let request = parse_request::<LlmImageRequestContract>(request)?;
|
||||||
|
ExecutableRequest::Image(Box::new(
|
||||||
|
request
|
||||||
|
.try_into()
|
||||||
|
.map_err(|error: napi::Error| RuntimeError::invalid_input(error.reason.clone()))?,
|
||||||
|
))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let (needs_tools, attachment_kinds, attachment_sources) = request_requirements(&executable);
|
let (needs_tools, attachment_kinds, attachment_sources) = request_requirements(&executable);
|
||||||
Ok((
|
Ok((
|
||||||
@@ -405,3 +413,46 @@ impl From<BackendError> for RuntimeError {
|
|||||||
RuntimeError::invalid_state(error.to_string())
|
RuntimeError::invalid_state(error.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::llm::route::slot;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn image_dispatch_accepts_napi_request_contract() {
|
||||||
|
let (_, request) = request_and_slot(
|
||||||
|
slot("action.image.filter.pixel").unwrap(),
|
||||||
|
json!({
|
||||||
|
"model": "gpt-image-1",
|
||||||
|
"prompt": "apply pixel filter",
|
||||||
|
"operation": "edit",
|
||||||
|
"images": [{
|
||||||
|
"kind": "data",
|
||||||
|
"dataBase64": "aW1n",
|
||||||
|
"mediaType": "image/png",
|
||||||
|
"fileName": "in.png"
|
||||||
|
}],
|
||||||
|
"options": {
|
||||||
|
"outputFormat": "webp",
|
||||||
|
"outputCompression": 80
|
||||||
|
},
|
||||||
|
"providerOptions": {
|
||||||
|
"provider": "openai",
|
||||||
|
"options": {
|
||||||
|
"input_fidelity": "high"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ExecutableRequest::Image(request) = request else {
|
||||||
|
panic!("image slot produced a non-image request");
|
||||||
|
};
|
||||||
|
assert!(request.is_edit());
|
||||||
|
assert_eq!(request.images()[0].media_type(), Some("image/png"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,15 +8,14 @@ use std::{
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub(in crate::runtime::backend_runtime) use dispatch::{
|
||||||
|
endpoint as byok_endpoint, protocol as executable_protocol, provider as backend_provider,
|
||||||
|
};
|
||||||
use gcp_auth::TokenProvider;
|
use gcp_auth::TokenProvider;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
pub(in crate::runtime::backend_runtime) use dispatch::{
|
|
||||||
endpoint as byok_endpoint, protocol as executable_protocol, provider as backend_provider,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{BackendRuntime, RuntimeError, RuntimeResult, to_napi_error};
|
use super::{BackendRuntime, RuntimeError, RuntimeResult, to_napi_error};
|
||||||
use crate::{
|
use crate::{
|
||||||
llm::{
|
llm::{
|
||||||
|
|||||||
@@ -330,7 +330,10 @@ fn default_mail_class_mapping() -> BTreeMap<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn load_app_config_overrides_from_db(pool: &PgPool) -> RuntimeResult<serde_json::Value> {
|
async fn load_app_config_overrides_from_db(pool: &PgPool) -> RuntimeResult<serde_json::Value> {
|
||||||
let rows = match sqlx::query("SELECT id, value FROM app_configs").fetch_all(pool).await {
|
let rows = match sqlx::query("SELECT id, value FROM app_configs ORDER BY id ASC")
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(rows) => rows,
|
Ok(rows) => rows,
|
||||||
Err(sqlx::Error::Database(err)) if err.code().as_deref() == Some("42P01") => {
|
Err(sqlx::Error::Database(err)) if err.code().as_deref() == Some("42P01") => {
|
||||||
return Ok(serde_json::Value::Object(Map::new()));
|
return Ok(serde_json::Value::Object(Map::new()));
|
||||||
@@ -360,6 +363,8 @@ where
|
|||||||
S: AsRef<str>,
|
S: AsRef<str>,
|
||||||
{
|
{
|
||||||
let mut root = Map::new();
|
let mut root = Map::new();
|
||||||
|
let mut rows = rows.into_iter().collect::<Vec<_>>();
|
||||||
|
rows.sort_by(|(left, _), (right, _)| left.as_ref().cmp(right.as_ref()));
|
||||||
for (path, value) in rows {
|
for (path, value) in rows {
|
||||||
insert_flat_override(&mut root, path.as_ref(), value);
|
insert_flat_override(&mut root, path.as_ref(), value);
|
||||||
}
|
}
|
||||||
@@ -512,6 +517,22 @@ mod tests {
|
|||||||
assert_eq!(copilot.providers.profiles[0].id, "managed-openai");
|
assert_eq!(copilot.providers.profiles[0].id, "managed-openai");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_database_config_overrides_are_order_independent() {
|
||||||
|
let app_config = app_config_from_flat_overrides([
|
||||||
|
("copilot.byok.enabled", serde_json::json!(false)),
|
||||||
|
(
|
||||||
|
"copilot.byok",
|
||||||
|
serde_json::json!({ "enabled": true, "allowCustomEndpoint": true }),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
let byok = app_config.copilot.unwrap().byok;
|
||||||
|
|
||||||
|
assert!(!byok.enabled);
|
||||||
|
assert!(byok.allow_custom_endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn database_config_only_replaces_an_active_private_key_explicitly() {
|
fn database_config_only_replaces_an_active_private_key_explicitly() {
|
||||||
let active = BackendRuntimeConfig {
|
let active = BackendRuntimeConfig {
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ test('image request builder receives only serializable request options', async t
|
|||||||
runtime.streamImageArtifacts({}, [{ role: 'user', content: 'draw' }], {
|
runtime.streamImageArtifacts({}, [{ role: 'user', content: 'draw' }], {
|
||||||
quality: 'high',
|
quality: 'high',
|
||||||
seed: 42,
|
seed: 42,
|
||||||
|
modelName: 'stabilityai/stable-diffusion-xl-base-1.0',
|
||||||
|
loras: [{ path: 'https://example.com/sketch.safetensors', scale: 1 }],
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
user: 'user-1',
|
user: 'user-1',
|
||||||
})
|
})
|
||||||
@@ -158,6 +160,13 @@ test('image request builder receives only serializable request options', async t
|
|||||||
outputFormat: 'webp',
|
outputFormat: 'webp',
|
||||||
seed: 42,
|
seed: 42,
|
||||||
},
|
},
|
||||||
|
providerOptions: {
|
||||||
|
provider: 'fal',
|
||||||
|
options: {
|
||||||
|
model_name: 'stabilityai/stable-diffusion-xl-base-1.0',
|
||||||
|
loras: [{ path: 'https://example.com/sketch.safetensors', scale: 1 }],
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
import Sinon from 'sinon';
|
import Sinon from 'sinon';
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ const module = await createModule({
|
|||||||
const service = module.get(ServerService);
|
const service = module.get(ServerService);
|
||||||
const user = await module.create(Mockers.User);
|
const user = await module.create(Mockers.User);
|
||||||
const models = module.get(Models);
|
const models = module.get(Models);
|
||||||
|
const db = module.get(PrismaClient);
|
||||||
|
|
||||||
test.afterEach(async () => {
|
test.afterEach(async () => {
|
||||||
Sinon.reset();
|
Sinon.reset();
|
||||||
@@ -111,6 +113,40 @@ test('should revalidate config', async t => {
|
|||||||
t.is(service.getConfig().server.externalUrl, newValue);
|
t.is(service.getConfig().server.externalUrl, newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should reject overlapping app config paths in one update', async t => {
|
||||||
|
await t.throwsAsync(
|
||||||
|
models.appConfig.save(user.id, [
|
||||||
|
{ key: 'testOverlapRoot.branch', value: { enabled: true } },
|
||||||
|
{ key: 'testOverlapRoot.branch.enabled', value: false },
|
||||||
|
]),
|
||||||
|
{ message: /must not overlap/ }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should serialize concurrent overlapping app config updates', async t => {
|
||||||
|
const root = `testConcurrentOverlap.${faker.string.uuid()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = await Promise.allSettled([
|
||||||
|
models.appConfig.save(user.id, [{ key: root, value: { enabled: true } }]),
|
||||||
|
models.appConfig.save(user.id, [
|
||||||
|
{ key: `${root}.enabled`, value: false },
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
t.is(results.filter(result => result.status === 'fulfilled').length, 1);
|
||||||
|
t.is(results.filter(result => result.status === 'rejected').length, 1);
|
||||||
|
t.regex(
|
||||||
|
String(results.find(result => result.status === 'rejected')?.reason),
|
||||||
|
/must not overlap/
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await db.appConfig.deleteMany({
|
||||||
|
where: { id: { startsWith: root } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('should emit config changed event', async t => {
|
test('should emit config changed event', async t => {
|
||||||
const newUrl = faker.internet.url();
|
const newUrl = faker.internet.url();
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,34 @@ export class AppConfigModel extends BaseModel {
|
|||||||
async load(excludedKeys: string[] = []) {
|
async load(excludedKeys: string[] = []) {
|
||||||
return this.db.appConfig.findMany({
|
return this.db.appConfig.findMany({
|
||||||
where: excludedKeys.length ? { id: { notIn: excludedKeys } } : undefined,
|
where: excludedKeys.length ? { id: { notIn: excludedKeys } } : undefined,
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
async save(user: string, updates: Array<{ key: string; value: any }>) {
|
async save(user: string, updates: Array<{ key: string; value: any }>) {
|
||||||
|
await this.db
|
||||||
|
.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${'app-config-paths'}, 0))`;
|
||||||
|
const existing = await this.db.appConfig.findMany({
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
const updateKeys = updates.map(update => update.key);
|
||||||
|
for (const [index, key] of updateKeys.entries()) {
|
||||||
|
const overlappingKey = [
|
||||||
|
...existing.map(config => config.id),
|
||||||
|
...updateKeys.slice(0, index),
|
||||||
|
].find(
|
||||||
|
candidate =>
|
||||||
|
candidate !== key &&
|
||||||
|
(candidate.startsWith(`${key}.`) || key.startsWith(`${candidate}.`))
|
||||||
|
);
|
||||||
|
if (overlappingKey) {
|
||||||
|
throw new Error(
|
||||||
|
`App config paths must not overlap: ${overlappingKey} and ${key}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return await Promise.allSettled(
|
return await Promise.allSettled(
|
||||||
updates.map(async update => {
|
updates.map(async update => {
|
||||||
return this.db.appConfig.upsert({
|
return this.db.appConfig.upsert({
|
||||||
|
|||||||
@@ -398,13 +398,13 @@ export class CapabilityRuntime {
|
|||||||
_filter?: ProviderFilter,
|
_filter?: ProviderFilter,
|
||||||
slot = 'image.generate'
|
slot = 'image.generate'
|
||||||
): AsyncIterableIterator<NativeImageArtifact> {
|
): AsyncIterableIterator<NativeImageArtifact> {
|
||||||
const { quality, seed } = options;
|
const { quality, seed, modelName, loras } = options;
|
||||||
const result = (await this.execute(
|
const result = (await this.execute(
|
||||||
slot,
|
slot,
|
||||||
buildLlmImageRequestFromMessages({
|
buildLlmImageRequestFromMessages({
|
||||||
model: 'route-selected',
|
model: 'route-selected',
|
||||||
messages: preparePromptMessagesForNativeRequest(messages, true),
|
messages: preparePromptMessagesForNativeRequest(messages, true),
|
||||||
options: { quality, seed },
|
options: { quality, seed, modelName, loras },
|
||||||
}),
|
}),
|
||||||
cond,
|
cond,
|
||||||
options
|
options
|
||||||
|
|||||||
@@ -367,7 +367,6 @@ declare global {
|
|||||||
// TODO(@Peng): should be refactored to get rid of implement details (like messages, action, role, etc.)
|
// TODO(@Peng): should be refactored to get rid of implement details (like messages, action, role, etc.)
|
||||||
interface AIHistory {
|
interface AIHistory {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
tokens: number;
|
|
||||||
action: string | null;
|
action: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
messages: {
|
messages: {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import './ai-chat-composer-tip';
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
AIDraftService,
|
AIDraftService,
|
||||||
|
AIModelService,
|
||||||
AIToolsConfigService,
|
AIToolsConfigService,
|
||||||
} from '@affine/core/modules/ai-button';
|
} from '@affine/core/modules/ai-button';
|
||||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
|
||||||
import type {
|
import type {
|
||||||
ServerService,
|
ServerService,
|
||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
@@ -128,15 +128,15 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor aiModelService!: AIModelService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor subscriptionService!: SubscriptionService;
|
accessor subscriptionService!: SubscriptionService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor aiModelService!: AIModelService;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onAISubscribe!: () => Promise<void>;
|
accessor onAISubscribe!: () => Promise<void>;
|
||||||
|
|
||||||
@@ -183,9 +183,9 @@ export class AIChatComposer extends SignalWatcher(
|
|||||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||||
.aiDraftService=${this.aiDraftService}
|
.aiDraftService=${this.aiDraftService}
|
||||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||||
|
.aiModelService=${this.aiModelService}
|
||||||
.notificationService=${this.notificationService}
|
.notificationService=${this.notificationService}
|
||||||
.subscriptionService=${this.subscriptionService}
|
.subscriptionService=${this.subscriptionService}
|
||||||
.aiModelService=${this.aiModelService}
|
|
||||||
.onAISubscribe=${this.onAISubscribe}
|
.onAISubscribe=${this.onAISubscribe}
|
||||||
.portalContainer=${this.portalContainer}
|
.portalContainer=${this.portalContainer}
|
||||||
.onChatSuccess=${this.onChatSuccess}
|
.onChatSuccess=${this.onChatSuccess}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
AIDraftService,
|
AIDraftService,
|
||||||
|
AIModelService,
|
||||||
AIToolsConfigService,
|
AIToolsConfigService,
|
||||||
} from '@affine/core/modules/ai-button';
|
} from '@affine/core/modules/ai-button';
|
||||||
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
|
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
|
||||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
|
||||||
import type {
|
import type {
|
||||||
ServerService,
|
ServerService,
|
||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
@@ -404,8 +404,8 @@ export class AIChatContent extends SignalWatcher(
|
|||||||
.notificationService=${this.notificationService}
|
.notificationService=${this.notificationService}
|
||||||
.aiDraftService=${this.aiDraftService}
|
.aiDraftService=${this.aiDraftService}
|
||||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||||
.subscriptionService=${this.subscriptionService}
|
|
||||||
.aiModelService=${this.aiModelService}
|
.aiModelService=${this.aiModelService}
|
||||||
|
.subscriptionService=${this.subscriptionService}
|
||||||
.onAISubscribe=${this.onAISubscribe}
|
.onAISubscribe=${this.onAISubscribe}
|
||||||
.trackOptions=${{
|
.trackOptions=${{
|
||||||
where: 'chat-panel',
|
where: 'chat-panel',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
AIDraftService,
|
AIDraftService,
|
||||||
|
AIModelService,
|
||||||
AIToolsConfigService,
|
AIToolsConfigService,
|
||||||
} from '@affine/core/modules/ai-button';
|
} from '@affine/core/modules/ai-button';
|
||||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
|
||||||
import type {
|
import type {
|
||||||
ServerService,
|
ServerService,
|
||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
@@ -399,6 +399,9 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor aiModelService!: AIModelService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||||
|
|
||||||
@@ -408,9 +411,6 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor subscriptionService!: SubscriptionService;
|
accessor subscriptionService!: SubscriptionService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor aiModelService!: AIModelService;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onAISubscribe!: () => Promise<void>;
|
accessor onAISubscribe!: () => Promise<void>;
|
||||||
|
|
||||||
@@ -483,6 +483,12 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
window.addEventListener('dragend', this._resetDragState);
|
window.addEventListener('dragend', this._resetDragState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override updated(changedProperties: PropertyValues<this>) {
|
||||||
|
if (changedProperties.has('workspaceId')) {
|
||||||
|
this.aiModelService.setScope(this.workspaceId, 'Chat With AFFiNE AI');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override firstUpdated(changedProperties: PropertyValues): void {
|
protected override firstUpdated(changedProperties: PropertyValues): void {
|
||||||
super.firstUpdated(changedProperties);
|
super.firstUpdated(changedProperties);
|
||||||
if (this.aiDraftService) {
|
if (this.aiDraftService) {
|
||||||
@@ -631,9 +637,9 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
.onExtendedThinkingChange=${this._toggleReasoning}
|
.onExtendedThinkingChange=${this._toggleReasoning}
|
||||||
.serverService=${this.serverService}
|
.serverService=${this.serverService}
|
||||||
.toolsConfigService=${this.aiToolsConfigService}
|
.toolsConfigService=${this.aiToolsConfigService}
|
||||||
|
.aiModelService=${this.aiModelService}
|
||||||
.notificationService=${this.notificationService}
|
.notificationService=${this.notificationService}
|
||||||
.subscriptionService=${this.subscriptionService}
|
.subscriptionService=${this.subscriptionService}
|
||||||
.aiModelService=${this.aiModelService}
|
|
||||||
.onAISubscribe=${this.onAISubscribe}
|
.onAISubscribe=${this.onAISubscribe}
|
||||||
></chat-input-preference>
|
></chat-input-preference>
|
||||||
${status === 'transmitting' || status === 'loading'
|
${status === 'transmitting' || status === 'loading'
|
||||||
@@ -858,7 +864,7 @@ export class AIChatInput extends SignalWatcher(
|
|||||||
control: this.trackOptions?.control,
|
control: this.trackOptions?.control,
|
||||||
reasoning: this._isReasoningActive,
|
reasoning: this._isReasoningActive,
|
||||||
toolsConfig: this.aiToolsConfigService.config.value,
|
toolsConfig: this.aiToolsConfigService.config.value,
|
||||||
modelId: this.aiModelService.modelId.value,
|
routeTargetId: this.aiModelService.modelId.value,
|
||||||
userInfo: {
|
userInfo: {
|
||||||
userId: userInfo?.id,
|
userId: userInfo?.id,
|
||||||
userName: userInfo?.name,
|
userName: userInfo?.name,
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
||||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
||||||
import type {
|
|
||||||
ServerService,
|
|
||||||
SubscriptionService,
|
|
||||||
} from '@affine/core/modules/cloud';
|
|
||||||
import {
|
|
||||||
type CopilotChatHistoryFragment,
|
|
||||||
ServerDeploymentType,
|
|
||||||
SubscriptionStatus,
|
|
||||||
} from '@affine/graphql';
|
|
||||||
import {
|
import {
|
||||||
menu,
|
menu,
|
||||||
popMenu,
|
popMenu,
|
||||||
@@ -70,10 +61,10 @@ export class ChatInputPreference extends SignalWatcher(
|
|||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
.ai-active-model-name {
|
.ai-active-model-name {
|
||||||
font-size: 14px;
|
|
||||||
color: ${unsafeCSSVarV2('text/secondary')};
|
|
||||||
line-height: 22px;
|
|
||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
|
color: ${unsafeCSSVarV2('text/secondary')};
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
.ai-model-prefix {
|
.ai-model-prefix {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@@ -82,21 +73,21 @@ export class ChatInputPreference extends SignalWatcher(
|
|||||||
.ai-model-prefix svg {
|
.ai-model-prefix svg {
|
||||||
color: ${unsafeCSSVarV2('icon/activated')};
|
color: ${unsafeCSSVarV2('icon/activated')};
|
||||||
}
|
}
|
||||||
|
.ai-model-postfix {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
.ai-model-postfix svg:hover {
|
.ai-model-postfix svg:hover {
|
||||||
color: ${unsafeCSSVarV2('icon/activated')};
|
color: ${unsafeCSSVarV2('icon/activated')};
|
||||||
}
|
}
|
||||||
.ai-model-version {
|
.ai-model-version {
|
||||||
font-size: 12px;
|
|
||||||
color: ${unsafeCSSVarV2('text/tertiary')};
|
|
||||||
line-height: 20px;
|
|
||||||
margin-right: 40px;
|
margin-right: 40px;
|
||||||
|
color: ${unsafeCSSVarV2('text/tertiary')};
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor session!: CopilotChatHistoryFragment | null | undefined;
|
|
||||||
// --------- model props end ---------
|
|
||||||
|
|
||||||
// --------- extended thinking props start ---------
|
// --------- extended thinking props start ---------
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor extendedThinking: boolean = false;
|
accessor extendedThinking: boolean = false;
|
||||||
@@ -107,90 +98,91 @@ export class ChatInputPreference extends SignalWatcher(
|
|||||||
| undefined;
|
| undefined;
|
||||||
// --------- extended thinking props end ---------
|
// --------- extended thinking props end ---------
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor serverService!: ServerService;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor toolsConfigService!: AIToolsConfigService;
|
accessor toolsConfigService!: AIToolsConfigService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor notificationService!: NotificationService;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor subscriptionService!: SubscriptionService;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor aiModelService!: AIModelService;
|
accessor aiModelService!: AIModelService;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor notificationService!: NotificationService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onAISubscribe!: () => Promise<void>;
|
accessor onAISubscribe!: () => Promise<void>;
|
||||||
|
|
||||||
model = computed(() => {
|
private readonly model = computed(() =>
|
||||||
const modelId = this.aiModelService.modelId.value;
|
this.aiModelService.models.value.find(
|
||||||
const activeModel = this.aiModelService.models.value.find(
|
model => model.id === this.aiModelService.modelId.value
|
||||||
model => model.id === modelId
|
)
|
||||||
);
|
);
|
||||||
const defaultModel = this.aiModelService.models.value.find(
|
|
||||||
model => model.isDefault
|
|
||||||
);
|
|
||||||
return activeModel || defaultModel;
|
|
||||||
});
|
|
||||||
|
|
||||||
openPreference(e: Event) {
|
openPreference(e: Event) {
|
||||||
const element = e.currentTarget;
|
const element = e.currentTarget;
|
||||||
if (!(element instanceof HTMLElement)) return;
|
if (!(element instanceof HTMLElement)) return;
|
||||||
const modelItems = [];
|
const preferenceItems = [];
|
||||||
const searchItems = [];
|
const searchItems = [];
|
||||||
|
|
||||||
// model switch
|
if (this.aiModelService.models.value.length) {
|
||||||
modelItems.push(
|
preferenceItems.push(
|
||||||
menu.subMenu({
|
menu.subMenu({
|
||||||
name: 'Model',
|
name: 'Model',
|
||||||
prefix: AiOutlineIcon(),
|
prefix: AiOutlineIcon(),
|
||||||
middleware: modelSubMenuMiddleware,
|
middleware: modelSubMenuMiddleware,
|
||||||
postfix: html`
|
postfix: html`
|
||||||
<span class="ai-active-model-name"> ${this.model.value?.name} </span>
|
<span class="ai-active-model-name">
|
||||||
`,
|
${this.model.value?.name ?? 'Auto'}
|
||||||
options: {
|
</span>
|
||||||
items: this.aiModelService.models.value.map(model => {
|
`,
|
||||||
const isSelected = model.id === this.model.value?.id;
|
options: {
|
||||||
const isSelfHosted =
|
items: [
|
||||||
this.serverService.server.config$.value?.type ===
|
menu.action({
|
||||||
ServerDeploymentType.Selfhosted;
|
name: 'Auto',
|
||||||
const status =
|
prefix: html`
|
||||||
this.subscriptionService.subscription.ai$.value?.status;
|
<div class="ai-model-prefix">
|
||||||
const isSubscribed = status === SubscriptionStatus.Active;
|
${this.aiModelService.modelId.value
|
||||||
return menu.action({
|
? undefined
|
||||||
name: model.category,
|
: DoneIcon()}
|
||||||
info: html`
|
</div>
|
||||||
<span class="ai-model-version">${model.version}</span>
|
`,
|
||||||
`,
|
select: () => this.aiModelService.resetModel(),
|
||||||
prefix: html`
|
}),
|
||||||
<div class="ai-model-prefix">
|
...this.aiModelService.models.value.map(model =>
|
||||||
${isSelected ? DoneIcon() : undefined}
|
menu.action({
|
||||||
</div>
|
name: model.category,
|
||||||
`,
|
info: html`
|
||||||
postfix: html`
|
<span class="ai-model-version">${model.version}</span>
|
||||||
<div class="ai-model-postfix" @click=${this.onAISubscribe}>
|
`,
|
||||||
${model.isPro && !isSubscribed ? LockIcon() : undefined}
|
prefix: html`
|
||||||
</div>
|
<div class="ai-model-prefix">
|
||||||
`,
|
${model.id === this.aiModelService.modelId.value
|
||||||
select: () => {
|
? DoneIcon()
|
||||||
if (model.isPro && !isSelfHosted && !isSubscribed) {
|
: undefined}
|
||||||
this.notificationService.toast(
|
</div>
|
||||||
`Pro models require an AFFiNE AI subscription.`
|
`,
|
||||||
);
|
postfix: html`
|
||||||
return;
|
<div class="ai-model-postfix">
|
||||||
}
|
${model.available ? undefined : LockIcon()}
|
||||||
this.aiModelService.setModel(model.id);
|
</div>
|
||||||
},
|
`,
|
||||||
});
|
select: () => {
|
||||||
}),
|
if (!model.available) {
|
||||||
},
|
this.notificationService.toast(
|
||||||
})
|
'This model requires an AFFiNE AI subscription.'
|
||||||
);
|
);
|
||||||
|
this.onAISubscribe().catch(console.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.aiModelService.setModel(model.id);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
modelItems.push(
|
preferenceItems.push(
|
||||||
menu.toggleSwitch({
|
menu.toggleSwitch({
|
||||||
name: 'Extended Thinking',
|
name: 'Extended Thinking',
|
||||||
prefix: ThinkingIcon(),
|
prefix: ThinkingIcon(),
|
||||||
@@ -220,7 +212,7 @@ export class ChatInputPreference extends SignalWatcher(
|
|||||||
options: {
|
options: {
|
||||||
items: [
|
items: [
|
||||||
menu.group({
|
menu.group({
|
||||||
items: [...modelItems],
|
items: [...preferenceItems],
|
||||||
}),
|
}),
|
||||||
menu.group({
|
menu.group({
|
||||||
items: [...searchItems],
|
items: [...searchItems],
|
||||||
@@ -238,7 +230,7 @@ export class ChatInputPreference extends SignalWatcher(
|
|||||||
class="chat-input-preference-trigger"
|
class="chat-input-preference-trigger"
|
||||||
>
|
>
|
||||||
<span class="chat-input-preference-trigger-label">
|
<span class="chat-input-preference-trigger-label">
|
||||||
${this.model.value?.category}
|
${this.model.value?.category ?? 'Auto'}
|
||||||
</span>
|
</span>
|
||||||
<span class="chat-input-preference-trigger-icon">
|
<span class="chat-input-preference-trigger-icon">
|
||||||
${ArrowDownSmallIcon()}
|
${ArrowDownSmallIcon()}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
import type {
|
||||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
AIModelService,
|
||||||
|
AIToolsConfigService,
|
||||||
|
} from '@affine/core/modules/ai-button';
|
||||||
import type {
|
import type {
|
||||||
ServerService,
|
ServerService,
|
||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
@@ -185,10 +187,10 @@ export class PlaygroundChat extends SignalWatcher(
|
|||||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor subscriptionService!: SubscriptionService;
|
accessor aiModelService!: AIModelService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor aiModelService!: AIModelService;
|
accessor subscriptionService!: SubscriptionService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onAISubscribe: (() => Promise<void>) | undefined;
|
accessor onAISubscribe: (() => Promise<void>) | undefined;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
|
import type {
|
||||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
AIModelService,
|
||||||
|
AIToolsConfigService,
|
||||||
|
} from '@affine/core/modules/ai-button';
|
||||||
import type {
|
import type {
|
||||||
ServerService,
|
ServerService,
|
||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
@@ -103,15 +105,15 @@ export class PlaygroundContent extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor aiToolsConfigService!: AIToolsConfigService;
|
accessor aiToolsConfigService!: AIToolsConfigService;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor aiModelService!: AIModelService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor subscriptionService!: SubscriptionService;
|
accessor subscriptionService!: SubscriptionService;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor aiModelService!: AIModelService;
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor sessions: CopilotChatHistoryFragment[] = [];
|
accessor sessions: CopilotChatHistoryFragment[] = [];
|
||||||
|
|
||||||
@@ -380,10 +382,10 @@ export class PlaygroundContent extends SignalWatcher(
|
|||||||
.affineThemeService=${this.affineThemeService}
|
.affineThemeService=${this.affineThemeService}
|
||||||
.notificationService=${this.notificationService}
|
.notificationService=${this.notificationService}
|
||||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||||
|
.aiModelService=${this.aiModelService}
|
||||||
.affineWorkspaceDialogService=${this
|
.affineWorkspaceDialogService=${this
|
||||||
.affineWorkspaceDialogService}
|
.affineWorkspaceDialogService}
|
||||||
.subscriptionService=${this.subscriptionService}
|
.subscriptionService=${this.subscriptionService}
|
||||||
.aiModelService=${this.aiModelService}
|
|
||||||
.addChat=${this.addChat}
|
.addChat=${this.addChat}
|
||||||
></playground-chat>
|
></playground-chat>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
AIDraftService,
|
AIDraftService,
|
||||||
|
AIModelService,
|
||||||
AIToolsConfigService,
|
AIToolsConfigService,
|
||||||
} from '@affine/core/modules/ai-button';
|
} from '@affine/core/modules/ai-button';
|
||||||
import type { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
|
||||||
import type {
|
import type {
|
||||||
ServerService,
|
ServerService,
|
||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
@@ -584,6 +584,7 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
|
||||||
.notificationService=${notificationService}
|
.notificationService=${notificationService}
|
||||||
.aiToolsConfigService=${this.aiToolsConfigService}
|
.aiToolsConfigService=${this.aiToolsConfigService}
|
||||||
|
.aiModelService=${this.aiModelService}
|
||||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||||
.onChatSuccess=${this._onChatSuccess}
|
.onChatSuccess=${this._onChatSuccess}
|
||||||
.trackOptions=${{
|
.trackOptions=${{
|
||||||
@@ -594,7 +595,6 @@ export class AIChatBlockPeekView extends LitElement {
|
|||||||
.reasoningConfig=${this.reasoningConfig}
|
.reasoningConfig=${this.reasoningConfig}
|
||||||
.serverService=${this.serverService}
|
.serverService=${this.serverService}
|
||||||
.subscriptionService=${this.subscriptionService}
|
.subscriptionService=${this.subscriptionService}
|
||||||
.aiModelService=${this.aiModelService}
|
|
||||||
.onAISubscribe=${this.onAISubscribe}
|
.onAISubscribe=${this.onAISubscribe}
|
||||||
></ai-chat-composer>
|
></ai-chat-composer>
|
||||||
</div> `;
|
</div> `;
|
||||||
@@ -681,8 +681,8 @@ export const AIChatBlockPeekViewTemplate = (
|
|||||||
affineWorkspaceDialogService: WorkspaceDialogService,
|
affineWorkspaceDialogService: WorkspaceDialogService,
|
||||||
aiDraftService: AIDraftService,
|
aiDraftService: AIDraftService,
|
||||||
aiToolsConfigService: AIToolsConfigService,
|
aiToolsConfigService: AIToolsConfigService,
|
||||||
subscriptionService: SubscriptionService,
|
|
||||||
aiModelService: AIModelService,
|
aiModelService: AIModelService,
|
||||||
|
subscriptionService: SubscriptionService,
|
||||||
onAISubscribe: (() => Promise<void>) | undefined
|
onAISubscribe: (() => Promise<void>) | undefined
|
||||||
) => {
|
) => {
|
||||||
return html`<ai-chat-block-peek-view
|
return html`<ai-chat-block-peek-view
|
||||||
@@ -696,8 +696,8 @@ export const AIChatBlockPeekViewTemplate = (
|
|||||||
.affineWorkspaceDialogService=${affineWorkspaceDialogService}
|
.affineWorkspaceDialogService=${affineWorkspaceDialogService}
|
||||||
.aiDraftService=${aiDraftService}
|
.aiDraftService=${aiDraftService}
|
||||||
.aiToolsConfigService=${aiToolsConfigService}
|
.aiToolsConfigService=${aiToolsConfigService}
|
||||||
.subscriptionService=${subscriptionService}
|
|
||||||
.aiModelService=${aiModelService}
|
.aiModelService=${aiModelService}
|
||||||
|
.subscriptionService=${subscriptionService}
|
||||||
.onAISubscribe=${onAISubscribe}
|
.onAISubscribe=${onAISubscribe}
|
||||||
></ai-chat-block-peek-view>`;
|
></ai-chat-block-peek-view>`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export type AIChatSendOptions = {
|
|||||||
control?: BlockSuitePresets.TrackerControl;
|
control?: BlockSuitePresets.TrackerControl;
|
||||||
reasoning?: boolean;
|
reasoning?: boolean;
|
||||||
toolsConfig?: unknown;
|
toolsConfig?: unknown;
|
||||||
modelId?: string;
|
routeTargetId?: string;
|
||||||
userInfo?: {
|
userInfo?: {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
@@ -45,7 +45,7 @@ export type AIChatAction =
|
|||||||
| { type: 'clearError' }
|
| { type: 'clearError' }
|
||||||
| { type: 'setComposerText'; text: string }
|
| { type: 'setComposerText'; text: string }
|
||||||
| { type: 'setReasoning'; reasoning: boolean }
|
| { type: 'setReasoning'; reasoning: boolean }
|
||||||
| { type: 'setModel'; modelId?: string }
|
| { type: 'setRouteTarget'; routeTargetId?: string }
|
||||||
| { type: 'addAttachment'; attachment: string | Blob | File }
|
| { type: 'addAttachment'; attachment: string | Blob | File }
|
||||||
| { type: 'removeAttachment'; index: number }
|
| { type: 'removeAttachment'; index: number }
|
||||||
| { type: 'addContextItem'; item: AIChatContextItem }
|
| { type: 'addContextItem'; item: AIChatContextItem }
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ function session(
|
|||||||
parentSessionId: null,
|
parentSessionId: null,
|
||||||
promptName: 'Chat With AFFiNE AI',
|
promptName: 'Chat With AFFiNE AI',
|
||||||
action: null,
|
action: null,
|
||||||
optionalModels: null,
|
|
||||||
tokens: 0,
|
|
||||||
...overrides,
|
...overrides,
|
||||||
} as CopilotChatHistoryFragment;
|
} as CopilotChatHistoryFragment;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ export class AIChatRuntime {
|
|||||||
case 'setReasoning':
|
case 'setReasoning':
|
||||||
this.updateComposer({ reasoning: action.reasoning });
|
this.updateComposer({ reasoning: action.reasoning });
|
||||||
return;
|
return;
|
||||||
case 'setModel':
|
case 'setRouteTarget':
|
||||||
this.updateComposer({ modelId: action.modelId });
|
this.updateComposer({ routeTargetId: action.routeTargetId });
|
||||||
return;
|
return;
|
||||||
case 'addAttachment':
|
case 'addAttachment':
|
||||||
this.updateComposer({
|
this.updateComposer({
|
||||||
@@ -388,7 +388,8 @@ export class AIChatRuntime {
|
|||||||
contextId: this.snapshot.composer.context.contextId,
|
contextId: this.snapshot.composer.context.contextId,
|
||||||
reasoning: options.reasoning ?? this.snapshot.composer.reasoning,
|
reasoning: options.reasoning ?? this.snapshot.composer.reasoning,
|
||||||
toolsConfig: options.toolsConfig ?? this.snapshot.composer.toolsConfig,
|
toolsConfig: options.toolsConfig ?? this.snapshot.composer.toolsConfig,
|
||||||
modelId: options.modelId ?? this.snapshot.composer.modelId,
|
routeTargetId:
|
||||||
|
options.routeTargetId ?? this.snapshot.composer.routeTargetId,
|
||||||
isRootSession: options.isRootSession,
|
isRootSession: options.isRootSession,
|
||||||
where: options.where,
|
where: options.where,
|
||||||
control: options.control,
|
control: options.control,
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export type AIChatComposerState = {
|
|||||||
context: AIChatContextState;
|
context: AIChatContextState;
|
||||||
reasoning: boolean;
|
reasoning: boolean;
|
||||||
toolsConfig?: AIToolsConfig;
|
toolsConfig?: AIToolsConfig;
|
||||||
modelId?: string;
|
routeTargetId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AIChatNavigationRequest = {
|
export type AIChatNavigationRequest = {
|
||||||
|
|||||||
@@ -67,9 +67,8 @@ export async function createWorkspaceByokLocalLease(
|
|||||||
provider: gqlProvider,
|
provider: gqlProvider,
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
description: provider.description ?? null,
|
description: provider.description ?? null,
|
||||||
apiKey: provider.apiKey,
|
credential: provider.credential,
|
||||||
endpoint: provider.endpoint ?? null,
|
definition: provider.definition,
|
||||||
sortOrder: provider.sortOrder ?? 0,
|
|
||||||
enabled: provider.enabled ?? true,
|
enabled: provider.enabled ?? true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -467,7 +467,9 @@ export class CopilotClient {
|
|||||||
sessionId,
|
sessionId,
|
||||||
messageId,
|
messageId,
|
||||||
reasoning,
|
reasoning,
|
||||||
|
profileId,
|
||||||
modelId,
|
modelId,
|
||||||
|
routeTargetId,
|
||||||
toolsConfig,
|
toolsConfig,
|
||||||
actionId,
|
actionId,
|
||||||
actionVersion,
|
actionVersion,
|
||||||
@@ -478,7 +480,9 @@ export class CopilotClient {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
reasoning?: boolean;
|
reasoning?: boolean;
|
||||||
|
profileId?: string;
|
||||||
modelId?: string;
|
modelId?: string;
|
||||||
|
routeTargetId?: string;
|
||||||
toolsConfig?: AIToolsConfig;
|
toolsConfig?: AIToolsConfig;
|
||||||
actionId?: string;
|
actionId?: string;
|
||||||
actionVersion?: string;
|
actionVersion?: string;
|
||||||
@@ -495,7 +499,9 @@ export class CopilotClient {
|
|||||||
const queryString = this.paramsToQueryString({
|
const queryString = this.paramsToQueryString({
|
||||||
messageId,
|
messageId,
|
||||||
reasoning,
|
reasoning,
|
||||||
|
profileId,
|
||||||
modelId,
|
modelId,
|
||||||
|
routeTargetId,
|
||||||
toolsConfig,
|
toolsConfig,
|
||||||
actionId,
|
actionId,
|
||||||
actionVersion,
|
actionVersion,
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export type TextToTextOptions = {
|
|||||||
runId?: string;
|
runId?: string;
|
||||||
isRootSession?: boolean;
|
isRootSession?: boolean;
|
||||||
reasoning?: boolean;
|
reasoning?: boolean;
|
||||||
|
profileId?: string;
|
||||||
modelId?: string;
|
modelId?: string;
|
||||||
|
routeTargetId?: string;
|
||||||
toolsConfig?: AIToolsConfig;
|
toolsConfig?: AIToolsConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,7 +129,9 @@ export function textToText({
|
|||||||
actionVersion,
|
actionVersion,
|
||||||
runId,
|
runId,
|
||||||
reasoning,
|
reasoning,
|
||||||
|
profileId,
|
||||||
modelId,
|
modelId,
|
||||||
|
routeTargetId,
|
||||||
toolsConfig,
|
toolsConfig,
|
||||||
}: TextToTextOptions) {
|
}: TextToTextOptions) {
|
||||||
let messageId: string | undefined;
|
let messageId: string | undefined;
|
||||||
@@ -161,7 +165,9 @@ export function textToText({
|
|||||||
sessionId,
|
sessionId,
|
||||||
messageId,
|
messageId,
|
||||||
reasoning,
|
reasoning,
|
||||||
|
profileId,
|
||||||
modelId,
|
modelId,
|
||||||
|
routeTargetId,
|
||||||
toolsConfig,
|
toolsConfig,
|
||||||
actionId,
|
actionId,
|
||||||
actionVersion,
|
actionVersion,
|
||||||
@@ -229,7 +235,9 @@ export function textToText({
|
|||||||
sessionId,
|
sessionId,
|
||||||
messageId,
|
messageId,
|
||||||
reasoning,
|
reasoning,
|
||||||
|
profileId,
|
||||||
modelId,
|
modelId,
|
||||||
|
routeTargetId,
|
||||||
toolsConfig,
|
toolsConfig,
|
||||||
actionId,
|
actionId,
|
||||||
actionVersion,
|
actionVersion,
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ const electronApis = vi.hoisted(() => ({
|
|||||||
Array<{
|
Array<{
|
||||||
provider: string;
|
provider: string;
|
||||||
name: string;
|
name: string;
|
||||||
apiKey: string;
|
credential: string;
|
||||||
|
definition: {
|
||||||
|
version: number;
|
||||||
|
endpoint: { kind: string; url?: string | null };
|
||||||
|
models: unknown[];
|
||||||
|
};
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
endpoint?: string | null;
|
|
||||||
sortOrder?: number | null;
|
sortOrder?: number | null;
|
||||||
enabled?: boolean | null;
|
enabled?: boolean | null;
|
||||||
}>
|
}>
|
||||||
@@ -117,7 +121,12 @@ describe('runtime request transport BYOK local lease handling', () => {
|
|||||||
{
|
{
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
name: 'OpenAI',
|
name: 'OpenAI',
|
||||||
apiKey: 'sk-local',
|
credential: 'sk-local',
|
||||||
|
definition: {
|
||||||
|
version: 1,
|
||||||
|
endpoint: { kind: 'provider_default' },
|
||||||
|
models: [{ modelId: 'model-1', capabilities: [] }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Button, Modal, notify } from '@affine/component';
|
import { Button, Input, Modal, notify } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
ByokKeyStorage,
|
|
||||||
ByokProvider,
|
ByokProvider,
|
||||||
testWorkspaceByokConfigMutation as testByokMutation,
|
createWorkspaceByokProfileMutation,
|
||||||
upsertWorkspaceByokConfigMutation as upsertByokMutation,
|
probeWorkspaceByokDraftMutation,
|
||||||
|
replaceWorkspaceByokProfileMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { logByokError } from './errors';
|
import { logByokError } from './errors';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
@@ -18,13 +18,16 @@ import {
|
|||||||
shouldShowEndpoint,
|
shouldShowEndpoint,
|
||||||
storageLabel,
|
storageLabel,
|
||||||
} from './metadata';
|
} from './metadata';
|
||||||
import type {
|
import { ModelSelector } from './model-selector';
|
||||||
ByokKey,
|
import {
|
||||||
ByokSettings,
|
catalogModels,
|
||||||
ByokStorage,
|
defaultModels,
|
||||||
ByokTestResult,
|
type ModelDeclaration,
|
||||||
GqlFn,
|
modelUseCases,
|
||||||
} from './types';
|
probeChecks,
|
||||||
|
} from './model-utils';
|
||||||
|
import type { ByokDefinition, ByokKey, ByokSettings, GqlFn } from './types';
|
||||||
|
import { ByokStorage } from './types';
|
||||||
|
|
||||||
export const AddKeyModal = ({
|
export const AddKeyModal = ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -59,101 +62,133 @@ export const AddKeyModal = ({
|
|||||||
const [provider, setProvider] = useState<ByokProvider>(ByokProvider.openai);
|
const [provider, setProvider] = useState<ByokProvider>(ByokProvider.openai);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [storage, setStorage] = useState<ByokStorage>(ByokKeyStorage.server);
|
const [profileEnabled, setProfileEnabled] = useState(true);
|
||||||
|
const [storage, setStorage] = useState<ByokStorage>(ByokStorage.server);
|
||||||
const [apiKey, setApiKey] = useState('');
|
const [apiKey, setApiKey] = useState('');
|
||||||
|
const [customEndpoint, setCustomEndpoint] = useState(false);
|
||||||
const [endpoint, setEndpoint] = useState('');
|
const [endpoint, setEndpoint] = useState('');
|
||||||
const [testResult, setTestResult] = useState<ByokTestResult | null>(null);
|
const [models, setModels] = useState<ModelDeclaration[]>([]);
|
||||||
const [testing, setTesting] = useState(false);
|
const [testStatus, setTestStatus] = useState<'passed' | 'failed' | null>(
|
||||||
const canTestStoredConfig =
|
null
|
||||||
storage === ByokKeyStorage.server &&
|
);
|
||||||
editingKey?.storage === ByokKeyStorage.server &&
|
const [includeImageProbe, setIncludeImageProbe] = useState(false);
|
||||||
editingKey.provider === provider;
|
const [busy, setBusy] = useState(false);
|
||||||
const canTest = !!apiKey || canTestStoredConfig;
|
const busyRef = useRef(false);
|
||||||
|
const localStorageUnavailable = !localStorageSupported || !canAddLocalKey;
|
||||||
|
const localStorageDisabled = !!editingKey || localStorageUnavailable;
|
||||||
|
const showCustomEndpoint = shouldShowEndpoint(
|
||||||
|
isSelfHosted,
|
||||||
|
settings.customEndpointSupported
|
||||||
|
);
|
||||||
|
|
||||||
const endpointHint = endpointHintKey(
|
const endpointHint = endpointHintKey(
|
||||||
settings.customEndpointSupported,
|
settings.customEndpointSupported,
|
||||||
settings.privateEndpointSupported
|
settings.privateEndpointSupported
|
||||||
);
|
);
|
||||||
|
const providerCatalog = useMemo(
|
||||||
|
() => catalogModels(settings, provider),
|
||||||
|
[provider, settings]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) return;
|
||||||
return;
|
const nextProvider = editingKey?.provider ?? ByokProvider.openai;
|
||||||
}
|
setProvider(nextProvider);
|
||||||
setProvider(editingKey?.provider ?? ByokProvider.openai);
|
setName(editingKey?.name ?? providerLabels[nextProvider]);
|
||||||
setName(editingKey?.name ?? '');
|
|
||||||
setDescription(editingKey?.description ?? '');
|
setDescription(editingKey?.description ?? '');
|
||||||
|
setProfileEnabled(editingKey?.enabled ?? true);
|
||||||
setStorage(
|
setStorage(
|
||||||
editingKey?.storage ??
|
editingKey?.storage ??
|
||||||
(canAddServerKey ? ByokKeyStorage.server : ByokKeyStorage.local)
|
(canAddServerKey ? ByokStorage.server : ByokStorage.local)
|
||||||
);
|
);
|
||||||
setApiKey('');
|
setApiKey('');
|
||||||
setEndpoint(editingKey?.endpoint ?? '');
|
setEndpoint(editingKey?.definition.endpoint.url ?? '');
|
||||||
setTestResult(null);
|
setCustomEndpoint(editingKey?.definition.endpoint.kind === 'custom');
|
||||||
}, [canAddServerKey, editingKey, open]);
|
setModels(
|
||||||
|
editingKey?.definition.models ?? defaultModels(settings, nextProvider)
|
||||||
|
);
|
||||||
|
setTestStatus(null);
|
||||||
|
setIncludeImageProbe(false);
|
||||||
|
}, [canAddServerKey, editingKey, open, settings]);
|
||||||
|
|
||||||
const testKey = useCallback(async () => {
|
const definition = useMemo<ByokDefinition>(
|
||||||
if (!gql) {
|
() => ({
|
||||||
return;
|
version: editingKey?.definition.version ?? 1,
|
||||||
}
|
endpoint: customEndpoint
|
||||||
setTesting(true);
|
? { kind: 'custom', url: endpoint }
|
||||||
try {
|
: { kind: 'provider_default', url: null },
|
||||||
const result = await gql({
|
models,
|
||||||
query: testByokMutation,
|
}),
|
||||||
variables: {
|
[customEndpoint, editingKey?.definition.version, endpoint, models]
|
||||||
input: {
|
);
|
||||||
workspaceId,
|
|
||||||
provider,
|
const invalidateTest = () => setTestStatus(null);
|
||||||
storage,
|
const runProbe = useCallback(async () => {
|
||||||
apiKey: apiKey || null,
|
if (!gql) return false;
|
||||||
endpoint: endpoint || null,
|
const canReuseServerCredential =
|
||||||
configId: canTestStoredConfig ? editingKey.id : null,
|
editingKey?.storage === ByokStorage.server && !apiKey;
|
||||||
},
|
const checks = probeChecks(models, includeImageProbe);
|
||||||
|
const result = await gql({
|
||||||
|
query: probeWorkspaceByokDraftMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
workspaceId,
|
||||||
|
provider,
|
||||||
|
credential: apiKey || null,
|
||||||
|
profileId: canReuseServerCredential ? editingKey.id : null,
|
||||||
|
expectedRevision: canReuseServerCredential
|
||||||
|
? (editingKey.revision ?? null)
|
||||||
|
: null,
|
||||||
|
definition,
|
||||||
|
checks,
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
const nextResult = result.testWorkspaceByokConfig as
|
});
|
||||||
| ByokTestResult
|
const probe = result.probeWorkspaceByokDraft;
|
||||||
| undefined;
|
const verifiedChecks = new Set(
|
||||||
setTestResult(nextResult ?? null);
|
probe.models.flatMap(model =>
|
||||||
if (nextResult && !nextResult.ok) {
|
model.checks
|
||||||
notify.error({
|
.filter(check => check.status.kind === 'verified')
|
||||||
title: byokT(t, 'notify.test-failed.title'),
|
.map(check => `${model.modelId}\0${check.operation}`)
|
||||||
message: nextResult.message,
|
)
|
||||||
});
|
);
|
||||||
}
|
const passed =
|
||||||
} finally {
|
checks.length > 0 &&
|
||||||
setTesting(false);
|
probe.connection.kind === 'verified' &&
|
||||||
}
|
checks.every(check =>
|
||||||
|
verifiedChecks.has(`${check.modelId}\0${check.operation}`)
|
||||||
|
);
|
||||||
|
setTestStatus(passed ? 'passed' : 'failed');
|
||||||
|
return passed;
|
||||||
}, [
|
}, [
|
||||||
apiKey,
|
apiKey,
|
||||||
canTestStoredConfig,
|
definition,
|
||||||
editingKey,
|
editingKey,
|
||||||
endpoint,
|
|
||||||
gql,
|
gql,
|
||||||
|
includeImageProbe,
|
||||||
|
models,
|
||||||
provider,
|
provider,
|
||||||
storage,
|
|
||||||
t,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const save = useCallback(async () => {
|
const persist = useCallback(async () => {
|
||||||
if (!testResult?.ok || !gql) {
|
if (!gql) return;
|
||||||
return;
|
if (storage === ByokStorage.local) {
|
||||||
}
|
|
||||||
if (storage === ByokKeyStorage.local) {
|
|
||||||
const saved = await upsertLocalKey(workspaceId, {
|
const saved = await upsertLocalKey(workspaceId, {
|
||||||
id:
|
id:
|
||||||
editingKey?.storage === ByokKeyStorage.local
|
editingKey?.storage === ByokStorage.local
|
||||||
? editingKey.id
|
? editingKey.id
|
||||||
: crypto.randomUUID(),
|
: crypto.randomUUID(),
|
||||||
provider,
|
provider,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
apiKey,
|
credential: apiKey,
|
||||||
endpoint: endpoint || null,
|
definition,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
editingKey?.storage === ByokKeyStorage.local
|
editingKey?.storage === ByokStorage.local
|
||||||
? editingKey.sortOrder
|
? editingKey.sortOrder
|
||||||
: localKeys.length,
|
: localKeys.length,
|
||||||
enabled: true,
|
enabled: profileEnabled,
|
||||||
});
|
});
|
||||||
if (!saved) {
|
if (!saved) {
|
||||||
notify.error({
|
notify.error({
|
||||||
@@ -163,189 +198,359 @@ export const AddKeyModal = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLocalKeys(await readLocalKeys(workspaceId));
|
setLocalKeys(await readLocalKeys(workspaceId));
|
||||||
} else {
|
} else if (editingKey?.storage === ByokStorage.server) {
|
||||||
|
if (editingKey.revision === undefined) {
|
||||||
|
notify.error({
|
||||||
|
title: byokT(t, 'notify.reload-required.title'),
|
||||||
|
message: byokT(t, 'notify.reload-required.message'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
await gql({
|
await gql({
|
||||||
query: upsertByokMutation,
|
query: replaceWorkspaceByokProfileMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
workspaceId,
|
||||||
|
profileId: editingKey.id,
|
||||||
|
expectedRevision: editingKey.revision,
|
||||||
|
name,
|
||||||
|
description: description || null,
|
||||||
|
credential: apiKey || null,
|
||||||
|
definition,
|
||||||
|
enabled: profileEnabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await onSaved();
|
||||||
|
} else {
|
||||||
|
await gql({
|
||||||
|
query: createWorkspaceByokProfileMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
id:
|
|
||||||
editingKey?.storage === ByokKeyStorage.server
|
|
||||||
? editingKey.id
|
|
||||||
: null,
|
|
||||||
provider,
|
provider,
|
||||||
name,
|
name,
|
||||||
description,
|
description: description || null,
|
||||||
storage,
|
credential: apiKey,
|
||||||
apiKey: apiKey || null,
|
definition,
|
||||||
endpoint: endpoint || null,
|
enabled: profileEnabled,
|
||||||
enabled: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await onSaved();
|
await onSaved();
|
||||||
}
|
}
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
setApiKey('');
|
|
||||||
setTestResult(null);
|
|
||||||
}, [
|
}, [
|
||||||
apiKey,
|
apiKey,
|
||||||
|
definition,
|
||||||
description,
|
description,
|
||||||
editingKey,
|
editingKey,
|
||||||
endpoint,
|
|
||||||
gql,
|
gql,
|
||||||
localKeys,
|
localKeys.length,
|
||||||
name,
|
name,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSaved,
|
onSaved,
|
||||||
provider,
|
provider,
|
||||||
|
profileEnabled,
|
||||||
setLocalKeys,
|
setLocalKeys,
|
||||||
storage,
|
storage,
|
||||||
t,
|
t,
|
||||||
testResult?.ok,
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const connect = useCallback(async () => {
|
||||||
|
if (busyRef.current) return;
|
||||||
|
busyRef.current = true;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const passed = testStatus === 'passed' || (await runProbe());
|
||||||
|
if (!passed) {
|
||||||
|
notify.error({
|
||||||
|
title: byokT(t, 'notify.test-failed.title'),
|
||||||
|
message: byokT(t, 'notify.operation-failed.message'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await persist();
|
||||||
|
} finally {
|
||||||
|
busyRef.current = false;
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}, [persist, runProbe, t, testStatus]);
|
||||||
|
|
||||||
|
const testConnection = useCallback(async () => {
|
||||||
|
if (busyRef.current) return;
|
||||||
|
busyRef.current = true;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await runProbe();
|
||||||
|
} finally {
|
||||||
|
busyRef.current = false;
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}, [runProbe]);
|
||||||
|
|
||||||
|
const hasCredential = !!apiKey || editingKey?.storage === ByokStorage.server;
|
||||||
|
const valid =
|
||||||
|
!!name.trim() &&
|
||||||
|
hasCredential &&
|
||||||
|
models.length > 0 &&
|
||||||
|
models.every(model => model.modelId.trim() && model.capabilities.length) &&
|
||||||
|
new Set(models.map(model => model.modelId.trim())).size === models.length &&
|
||||||
|
(!customEndpoint || !!endpoint.trim());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
width={520}
|
width={640}
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
title={
|
title={byokT(
|
||||||
editingKey ? byokT(t, 'modal.edit-title') : byokT(t, 'modal.add-title')
|
t,
|
||||||
}
|
editingKey ? 'modal.manage-title' : 'modal.connect-title'
|
||||||
description={byokT(t, 'modal.description')}
|
)}
|
||||||
|
description={byokT(t, 'modal.connect-description')}
|
||||||
>
|
>
|
||||||
<div className={styles.form}>
|
<div className={styles.form}>
|
||||||
<label className={styles.field}>
|
<div className={styles.formSection}>
|
||||||
<span className={styles.label}>{byokT(t, 'field.provider')}</span>
|
<div className={styles.sectionTitle}>
|
||||||
<select
|
{byokT(t, 'section.connection')}
|
||||||
className={styles.input}
|
</div>
|
||||||
value={provider}
|
<label className={styles.field}>
|
||||||
onChange={event => {
|
<span className={styles.label}>{byokT(t, 'field.provider')}</span>
|
||||||
setProvider(event.target.value as ByokProvider);
|
<select
|
||||||
setTestResult(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{settings.allowedProviders.map(provider => (
|
|
||||||
<option key={provider} value={provider}>
|
|
||||||
{providerLabels[provider]}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className={styles.field}>
|
|
||||||
<span className={styles.label}>{byokT(t, 'field.key-name')}</span>
|
|
||||||
<input
|
|
||||||
className={styles.input}
|
|
||||||
value={name}
|
|
||||||
onChange={event => setName(event.target.value)}
|
|
||||||
placeholder={byokT(t, 'placeholder.key-name')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className={styles.field}>
|
|
||||||
<span className={styles.label}>{byokT(t, 'field.description')}</span>
|
|
||||||
<input
|
|
||||||
className={styles.input}
|
|
||||||
value={description}
|
|
||||||
onChange={event => setDescription(event.target.value)}
|
|
||||||
placeholder={byokT(t, 'placeholder.description')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className={styles.field}>
|
|
||||||
<span className={styles.label}>{byokT(t, 'field.storage')}</span>
|
|
||||||
<select
|
|
||||||
className={styles.input}
|
|
||||||
value={storage}
|
|
||||||
disabled={!!editingKey}
|
|
||||||
onChange={event => {
|
|
||||||
setStorage(event.target.value as ByokStorage);
|
|
||||||
setTestResult(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value={ByokKeyStorage.server} disabled={!canAddServerKey}>
|
|
||||||
{storageLabel(t, ByokKeyStorage.server)}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
value={ByokKeyStorage.local}
|
|
||||||
disabled={!localStorageSupported || !canAddLocalKey}
|
|
||||||
>
|
|
||||||
{canAddLocalKey
|
|
||||||
? byokT(t, 'storage.local-this-device')
|
|
||||||
: byokT(t, 'storage.local-desktop-only')}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className={styles.field}>
|
|
||||||
<span className={styles.label}>{byokT(t, 'field.api-key')}</span>
|
|
||||||
<input
|
|
||||||
className={styles.input}
|
|
||||||
value={apiKey}
|
|
||||||
onChange={event => {
|
|
||||||
setApiKey(event.target.value);
|
|
||||||
setTestResult(null);
|
|
||||||
}}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</label>{' '}
|
|
||||||
{shouldShowEndpoint(isSelfHosted, settings.customEndpointSupported) ? (
|
|
||||||
<label className={styles.endpointField}>
|
|
||||||
<span className={styles.label}>{byokT(t, 'field.endpoint')}</span>
|
|
||||||
<input
|
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
value={endpoint}
|
value={provider}
|
||||||
disabled={!settings.customEndpointSupported}
|
disabled={!!editingKey}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
setEndpoint(event.target.value);
|
const next = event.target.value as ByokProvider;
|
||||||
setTestResult(null);
|
setProvider(next);
|
||||||
|
setName(providerLabels[next]);
|
||||||
|
setModels(defaultModels(settings, next));
|
||||||
|
invalidateTest();
|
||||||
}}
|
}}
|
||||||
placeholder="https://api.example.com/v1"
|
>
|
||||||
|
{settings.allowedProviders.map(item => (
|
||||||
|
<option key={item} value={item}>
|
||||||
|
{providerLabels[item]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div className={styles.storageOptions}>
|
||||||
|
<label
|
||||||
|
className={styles.storageOption}
|
||||||
|
data-disabled={!canAddServerKey}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className={styles.storageRadio}
|
||||||
|
type="radio"
|
||||||
|
name="byok-storage"
|
||||||
|
checked={storage === ByokStorage.server}
|
||||||
|
disabled={!!editingKey || !canAddServerKey}
|
||||||
|
onChange={() => setStorage(ByokStorage.server)}
|
||||||
|
/>
|
||||||
|
<span className={styles.storageCopy}>
|
||||||
|
<strong>{storageLabel(t, ByokStorage.server)}</strong>
|
||||||
|
<small className={styles.storageDescription}>
|
||||||
|
{byokT(t, 'storage.server.description')}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className={styles.storageOption}
|
||||||
|
data-disabled={localStorageUnavailable}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className={styles.storageRadio}
|
||||||
|
type="radio"
|
||||||
|
name="byok-storage"
|
||||||
|
checked={storage === ByokStorage.local}
|
||||||
|
disabled={localStorageDisabled}
|
||||||
|
onChange={() => setStorage(ByokStorage.local)}
|
||||||
|
/>
|
||||||
|
<span className={styles.storageCopy}>
|
||||||
|
<strong>{storageLabel(t, ByokStorage.local)}</strong>
|
||||||
|
<small className={styles.storageDescription}>
|
||||||
|
{!BUILD_CONFIG.isElectron
|
||||||
|
? byokT(t, 'storage.local.desktop-only')
|
||||||
|
: !localStorageSupported
|
||||||
|
? byokT(t, 'storage.local.unavailable')
|
||||||
|
: byokT(t, 'storage.local.description')}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label className={styles.field}>
|
||||||
|
<span className={styles.label}>{byokT(t, 'field.api-key')}</span>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={value => {
|
||||||
|
setApiKey(value);
|
||||||
|
invalidateTest();
|
||||||
|
}}
|
||||||
|
type="password"
|
||||||
|
placeholder={
|
||||||
|
editingKey?.storage === ByokStorage.server
|
||||||
|
? byokT(t, 'placeholder.keep-current-key')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{endpointHint ? (
|
{storage === ByokStorage.local ? (
|
||||||
<span className={styles.fieldHint}>{byokT(t, endpointHint)}</span>
|
<span className={styles.fieldHint}>
|
||||||
|
{byokT(t, 'storage.local.test-disclosure')}
|
||||||
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</label>
|
</label>
|
||||||
|
{showCustomEndpoint ? (
|
||||||
|
<>
|
||||||
|
<label className={styles.checkboxRow}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={customEndpoint}
|
||||||
|
disabled={!settings.customEndpointSupported}
|
||||||
|
onChange={event => {
|
||||||
|
setCustomEndpoint(event.target.checked);
|
||||||
|
if (event.target.checked && !editingKey) setModels([]);
|
||||||
|
if (!event.target.checked)
|
||||||
|
setModels(defaultModels(settings, provider));
|
||||||
|
invalidateTest();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{byokT(t, 'endpoint.use-custom')}
|
||||||
|
</label>
|
||||||
|
{!settings.customEndpointSupported && endpointHint ? (
|
||||||
|
<span className={styles.fieldHint}>
|
||||||
|
{byokT(t, endpointHint)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{customEndpoint ? (
|
||||||
|
<label className={styles.endpointField}>
|
||||||
|
<span className={styles.label}>
|
||||||
|
{byokT(t, 'field.endpoint')}
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
value={endpoint}
|
||||||
|
onChange={value => {
|
||||||
|
setEndpoint(value);
|
||||||
|
invalidateTest();
|
||||||
|
}}
|
||||||
|
placeholder="https://api.example.com/v1"
|
||||||
|
/>
|
||||||
|
{endpointHint ? (
|
||||||
|
<span className={styles.fieldHint}>
|
||||||
|
{byokT(t, endpointHint)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<div className={styles.sectionHeading}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.sectionTitle}>
|
||||||
|
{byokT(t, 'section.models')}
|
||||||
|
</div>
|
||||||
|
<div className={styles.description}>
|
||||||
|
{byokT(t, 'models.description.selected')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ModelSelector
|
||||||
|
customEndpoint={customEndpoint}
|
||||||
|
catalog={providerCatalog}
|
||||||
|
models={models}
|
||||||
|
validation={editingKey?.validation}
|
||||||
|
onChange={models => {
|
||||||
|
setModels(models);
|
||||||
|
invalidateTest();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details className={styles.advanced}>
|
||||||
|
<summary>{byokT(t, 'section.advanced')}</summary>
|
||||||
|
<div className={styles.advancedFields}>
|
||||||
|
<label className={styles.checkboxRow}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={profileEnabled}
|
||||||
|
onChange={event => setProfileEnabled(event.target.checked)}
|
||||||
|
/>
|
||||||
|
{byokT(t, 'field.provider-enabled')}
|
||||||
|
</label>
|
||||||
|
<label className={styles.field}>
|
||||||
|
<span className={styles.label}>{byokT(t, 'field.key-name')}</span>
|
||||||
|
<Input size="large" value={name} onChange={setName} />
|
||||||
|
</label>
|
||||||
|
<label className={styles.field}>
|
||||||
|
<span className={styles.label}>
|
||||||
|
{byokT(t, 'field.description')}
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
value={description}
|
||||||
|
onChange={setDescription}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
{models.some(model => modelUseCases(model).includes('image')) ? (
|
||||||
|
<label className={styles.checkboxRow}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={includeImageProbe}
|
||||||
|
onChange={event => {
|
||||||
|
setIncludeImageProbe(event.target.checked);
|
||||||
|
invalidateTest();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{byokT(t, 'probe.include-image')}
|
||||||
|
</label>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className={styles.modalActions}>
|
<div className={styles.modalActions}>
|
||||||
<span
|
<span
|
||||||
className={`${styles.testStatus} ${
|
className={`${styles.testStatus} ${
|
||||||
testResult?.ok
|
testStatus === 'passed'
|
||||||
? styles.success
|
? styles.success
|
||||||
: testResult && !testResult.ok
|
: testStatus === 'failed'
|
||||||
? styles.error
|
? styles.error
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{testResult?.ok
|
{testStatus === 'passed'
|
||||||
? byokT(t, 'status.key-verified')
|
? byokT(t, 'probe.verified')
|
||||||
: testResult
|
: testStatus === 'failed'
|
||||||
? byokT(t, 'status.key-test-failed')
|
? byokT(t, 'probe.failed')
|
||||||
: ''}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={!canTest || testing}
|
disabled={!valid || busy}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
testKey().catch(error => {
|
testConnection().catch(error => {
|
||||||
logByokError('Failed to test BYOK key', error);
|
logByokError('Failed to test BYOK provider', error);
|
||||||
notify.error({
|
setTestStatus('failed');
|
||||||
title: byokT(t, 'notify.test-failed.title'),
|
|
||||||
message: byokT(t, 'notify.operation-failed.message'),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{byokT(t, 'action.test-key')}
|
{byokT(t, 'action.test-connection')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
{byokT(t, 'action.cancel')}
|
{byokT(t, 'action.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!testResult?.ok || !name}
|
disabled={!valid || busy}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
save().catch(error => {
|
connect().catch(error => {
|
||||||
logByokError('Failed to save BYOK key', error);
|
logByokError('Failed to save BYOK provider', error);
|
||||||
notify.error({
|
notify.error({
|
||||||
title: byokT(t, 'notify.save-failed.title'),
|
title: byokT(t, 'notify.save-failed.title'),
|
||||||
message: byokT(t, 'notify.operation-failed.message'),
|
message: byokT(t, 'notify.operation-failed.message'),
|
||||||
@@ -353,7 +558,14 @@ export const AddKeyModal = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{byokT(t, 'action.save-key')}
|
{byokT(
|
||||||
|
t,
|
||||||
|
busy
|
||||||
|
? 'action.connecting'
|
||||||
|
: editingKey
|
||||||
|
? 'action.save-changes'
|
||||||
|
: 'action.connect'
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import { byokT, capabilityRows, warningDescription } from './metadata';
|
import { byokT, capabilityRows } from './metadata';
|
||||||
import type { ByokKey, ByokSettings } from './types';
|
import type { ByokKey } from './types';
|
||||||
|
|
||||||
function coverageIcon(
|
function coverageIcon(
|
||||||
icon: (typeof capabilityRows)[number]['icon']
|
icon: (typeof capabilityRows)[number]['icon']
|
||||||
@@ -47,13 +47,7 @@ function isRowCovered(row: (typeof capabilityRows)[number], keys: ByokKey[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CoveragePanel = ({
|
export const CoveragePanel = ({ keys }: { keys: ByokKey[] }) => {
|
||||||
keys,
|
|
||||||
settings,
|
|
||||||
}: {
|
|
||||||
keys: ByokKey[];
|
|
||||||
settings: ByokSettings;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -63,9 +57,6 @@ export const CoveragePanel = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.rows}>
|
<div className={styles.rows}>
|
||||||
{capabilityRows.map(row => {
|
{capabilityRows.map(row => {
|
||||||
const warning = settings.warnings.find(
|
|
||||||
w => w.featureKind === row.featureKind
|
|
||||||
);
|
|
||||||
const covered = isRowCovered(row, keys);
|
const covered = isRowCovered(row, keys);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -86,7 +77,7 @@ export const CoveragePanel = ({
|
|||||||
<div className={styles.rowMain}>
|
<div className={styles.rowMain}>
|
||||||
<div className={styles.rowTitle}>{byokT(t, row.titleKey)}</div>
|
<div className={styles.rowTitle}>{byokT(t, row.titleKey)}</div>
|
||||||
<div className={styles.rowDescription}>
|
<div className={styles.rowDescription}>
|
||||||
{warningDescription(t, warning) ?? byokT(t, row.fallbackKey)}
|
{byokT(t, row.fallbackKey)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const capabilityIcon = style({
|
|||||||
|
|
||||||
export const capabilityIconActive = style({
|
export const capabilityIconActive = style({
|
||||||
color: cssVarV2('button/primary'),
|
color: cssVarV2('button/primary'),
|
||||||
background: '#f0f7ff',
|
background: cssVarV2('chip/label/blue'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const capabilityIconSvg = style({
|
export const capabilityIconSvg = style({
|
||||||
@@ -176,14 +176,16 @@ export const locked = style({
|
|||||||
export const form = style({
|
export const form = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 12,
|
gap: 16,
|
||||||
|
maxHeight: 'min(720px, calc(100vh - 180px))',
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingRight: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const field = style({
|
export const field = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
height: '3em',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const endpointField = style([field, { height: 'auto' }]);
|
export const endpointField = style([field, { height: 'auto' }]);
|
||||||
@@ -201,6 +203,8 @@ export const fieldHint = style({
|
|||||||
|
|
||||||
export const input = style({
|
export const input = style({
|
||||||
height: 32,
|
height: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
maxHeight: 32,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
@@ -228,6 +232,319 @@ export const modalActions = style({
|
|||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
|
position: 'sticky',
|
||||||
|
bottom: 0,
|
||||||
|
paddingTop: 12,
|
||||||
|
background: cssVarV2('layer/background/primary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const formSection = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 12,
|
||||||
|
padding: 14,
|
||||||
|
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||||
|
borderRadius: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sectionHeading = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sectionTitle = style({
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
fontWeight: 600,
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const storageOptions = style({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const storageOption = style({
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
minHeight: 76,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
padding: 12,
|
||||||
|
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||||
|
borderRadius: 8,
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
cursor: 'pointer',
|
||||||
|
selectors: {
|
||||||
|
'&:has(input:checked)': {
|
||||||
|
borderColor: cssVarV2('button/primary'),
|
||||||
|
background: cssVarV2('layer/background/secondary'),
|
||||||
|
},
|
||||||
|
'&:has(input:focus-visible)': {
|
||||||
|
boxShadow: '0px 0px 0px 2px rgba(30, 150, 235, 0.30)',
|
||||||
|
},
|
||||||
|
'&[data-disabled="true"]': {
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
color: cssVarV2('text/disable'),
|
||||||
|
background: cssVarV2('layer/background/secondary'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const storageRadio = style({
|
||||||
|
position: 'absolute',
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
margin: 0,
|
||||||
|
opacity: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const storageCopy = style({
|
||||||
|
display: 'flex',
|
||||||
|
minWidth: 0,
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
lineHeight: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const storageDescription = style({
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
selectors: {
|
||||||
|
[`${storageOption}[data-disabled="true"] &`]: {
|
||||||
|
color: cssVarV2('text/disable'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const checkboxRow = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelToolbar = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectedModels = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 6,
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
listStyle: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectedModel = style({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '20px minmax(0, 1fr) auto auto auto',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
minHeight: 72,
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderRadius: 8,
|
||||||
|
background: cssVarV2('layer/background/secondary'),
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectedModelDisabled = style({
|
||||||
|
opacity: 0.58,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelDragHandle = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
cursor: 'grab',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelCopy = style({
|
||||||
|
display: 'flex',
|
||||||
|
minWidth: 0,
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelStatus = style({
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const recommended = style({
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: 999,
|
||||||
|
color: cssVarV2('button/primary'),
|
||||||
|
background: cssVarV2('chip/label/blue'),
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: '16px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelEmpty = style({
|
||||||
|
padding: '24px 16px',
|
||||||
|
borderRadius: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
background: cssVarV2('layer/background/secondary'),
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelModalBody = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 12,
|
||||||
|
maxHeight: 'min(440px, calc(100dvh - 220px))',
|
||||||
|
overflowY: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelModalDescription = style({
|
||||||
|
margin: '0 0 12px',
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
lineHeight: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelSearch = style({
|
||||||
|
flexShrink: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelFieldLabel = style({
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const catalogChoices = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
paddingBottom: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const catalogChoice = style({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '16px minmax(0, 1fr)',
|
||||||
|
alignItems: 'center',
|
||||||
|
columnGap: 10,
|
||||||
|
minHeight: 56,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
padding: '8px 10px',
|
||||||
|
border: '1px solid transparent',
|
||||||
|
borderRadius: 8,
|
||||||
|
background: cssVarV2('layer/background/secondary'),
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
cursor: 'pointer',
|
||||||
|
selectors: {
|
||||||
|
'&[data-selected="true"]': {
|
||||||
|
borderColor: cssVarV2('button/primary'),
|
||||||
|
background: cssVarV2('chip/label/blue'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelCheckbox = style({
|
||||||
|
flex: '0 0 auto',
|
||||||
|
fontSize: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const catalogModelCopy = style({
|
||||||
|
display: 'flex',
|
||||||
|
minWidth: 0,
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const catalogModelTitle = style({
|
||||||
|
display: 'flex',
|
||||||
|
minWidth: 0,
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
lineHeight: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const catalogModelMeta = style({
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
lineHeight: '16px',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelCapabilities = style({
|
||||||
|
minWidth: 0,
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
border: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useCaseGrid = style({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
||||||
|
columnGap: 16,
|
||||||
|
rowGap: 4,
|
||||||
|
marginTop: 8,
|
||||||
|
'@media': {
|
||||||
|
'(max-width: 600px)': {
|
||||||
|
gridTemplateColumns: '1fr',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelUseCase = style({
|
||||||
|
width: '100%',
|
||||||
|
minHeight: 28,
|
||||||
|
gap: 8,
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelUseCaseLabel = style({
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modelModalActions = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 12,
|
||||||
|
paddingTop: 12,
|
||||||
|
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const advanced = style({
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const advancedFields = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 10,
|
||||||
|
marginTop: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const inputStack = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const testStatus = style({
|
export const testStatus = style({
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,10 @@ import {
|
|||||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import {
|
import {
|
||||||
ByokKeyStorage,
|
deleteWorkspaceByokProfileMutation,
|
||||||
clearWorkspaceByokConfigsMutation as clearByokMutation,
|
|
||||||
deleteWorkspaceByokConfigMutation as deleteByokMutation,
|
|
||||||
type GraphQLQuery,
|
type GraphQLQuery,
|
||||||
|
probeWorkspaceByokProfileMutation,
|
||||||
|
reorderWorkspaceByokProfilesMutation,
|
||||||
ServerDeploymentType,
|
ServerDeploymentType,
|
||||||
workspaceByokSettingsQuery as byokSettingsQuery,
|
workspaceByokSettingsQuery as byokSettingsQuery,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
@@ -29,27 +29,12 @@ import {
|
|||||||
readLocalKeys,
|
readLocalKeys,
|
||||||
reorderLocalKeys,
|
reorderLocalKeys,
|
||||||
} from './local-storage';
|
} from './local-storage';
|
||||||
import { byokT } from './metadata';
|
import { byokT, capabilitiesFor } from './metadata';
|
||||||
import type {
|
import { probeChecks } from './model-utils';
|
||||||
ByokKey,
|
import type { ByokKey, ByokSettings, ByokUsagePoint, GqlFn } from './types';
|
||||||
ByokSettings,
|
import { ByokStorage } from './types';
|
||||||
ByokStorage,
|
|
||||||
ByokUsagePoint,
|
|
||||||
GqlFn,
|
|
||||||
} from './types';
|
|
||||||
import { UsagePanel } from './usage';
|
import { UsagePanel } from './usage';
|
||||||
|
|
||||||
const reorderByokMutation = {
|
|
||||||
id: 'reorderWorkspaceByokConfigsMutation',
|
|
||||||
op: 'reorderWorkspaceByokConfigs',
|
|
||||||
query: `mutation reorderWorkspaceByokConfigs($input: ReorderWorkspaceByokConfigsInput!) {
|
|
||||||
reorderWorkspaceByokConfigs(input: $input) {
|
|
||||||
id
|
|
||||||
sortOrder
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
} satisfies GraphQLQuery;
|
|
||||||
|
|
||||||
export const WorkspaceByokSetting = () => {
|
export const WorkspaceByokSetting = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
@@ -59,6 +44,7 @@ export const WorkspaceByokSetting = () => {
|
|||||||
const [localKeys, setLocalKeys] = useState<ByokKey[]>([]);
|
const [localKeys, setLocalKeys] = useState<ByokKey[]>([]);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editingKey, setEditingKey] = useState<ByokKey | null>(null);
|
const [editingKey, setEditingKey] = useState<ByokKey | null>(null);
|
||||||
|
const [testingKeyId, setTestingKeyId] = useState<string | null>(null);
|
||||||
const [draggingKey, setDraggingKey] = useState<{
|
const [draggingKey, setDraggingKey] = useState<{
|
||||||
id: string;
|
id: string;
|
||||||
storage: ByokStorage;
|
storage: ByokStorage;
|
||||||
@@ -83,8 +69,27 @@ export const WorkspaceByokSetting = () => {
|
|||||||
localByokStorageSupported(),
|
localByokStorageSupported(),
|
||||||
readLocalKeys(workspace.id),
|
readLocalKeys(workspace.id),
|
||||||
]);
|
]);
|
||||||
|
const serverKeys = data.workspace.byokSettings.profiles.map(profile => {
|
||||||
|
const key: ByokKey = {
|
||||||
|
id: profile.profileId,
|
||||||
|
provider: profile.provider,
|
||||||
|
name: profile.name,
|
||||||
|
description: profile.description,
|
||||||
|
storage: ByokStorage.server,
|
||||||
|
configured: true,
|
||||||
|
enabled: profile.enabled,
|
||||||
|
sortOrder: profile.sortOrder,
|
||||||
|
revision: profile.revision,
|
||||||
|
definition: profile.definition,
|
||||||
|
capabilities: [],
|
||||||
|
validation: profile.validation,
|
||||||
|
};
|
||||||
|
key.capabilities = capabilitiesFor(key);
|
||||||
|
return key;
|
||||||
|
});
|
||||||
setSettings({
|
setSettings({
|
||||||
...data.workspace.byokSettings,
|
...data.workspace.byokSettings,
|
||||||
|
keys: serverKeys,
|
||||||
localStorageSupported:
|
localStorageSupported:
|
||||||
data.workspace.byokSettings.localEntitled && localStorageSupported,
|
data.workspace.byokSettings.localEntitled && localStorageSupported,
|
||||||
});
|
});
|
||||||
@@ -105,7 +110,7 @@ export const WorkspaceByokSetting = () => {
|
|||||||
const keys = useMemo(() => {
|
const keys = useMemo(() => {
|
||||||
return [...localKeys, ...(settings?.keys ?? [])].toSorted((a, b) => {
|
return [...localKeys, ...(settings?.keys ?? [])].toSorted((a, b) => {
|
||||||
if (a.storage !== b.storage) {
|
if (a.storage !== b.storage) {
|
||||||
return a.storage === ByokKeyStorage.local ? -1 : 1;
|
return a.storage === ByokStorage.local ? -1 : 1;
|
||||||
}
|
}
|
||||||
return a.sortOrder - b.sortOrder;
|
return a.sortOrder - b.sortOrder;
|
||||||
});
|
});
|
||||||
@@ -123,26 +128,35 @@ export const WorkspaceByokSetting = () => {
|
|||||||
if (!settings) {
|
if (!settings) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!workspaceServer.server && settings.serverEntitled) {
|
const deletions: Promise<unknown>[] = [];
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (settings.serverEntitled && workspaceServer.server) {
|
if (settings.serverEntitled && workspaceServer.server) {
|
||||||
const gql = workspaceServer.server.gql as GqlFn;
|
const gql = workspaceServer.server.gql as GqlFn;
|
||||||
await gql({
|
deletions.push(
|
||||||
query: clearByokMutation,
|
...settings.keys.map(key =>
|
||||||
variables: { workspaceId: workspace.id },
|
gql({
|
||||||
});
|
query: deleteWorkspaceByokProfileMutation,
|
||||||
|
variables: { workspaceId: workspace.id, profileId: key.id },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (settings.localStorageSupported) {
|
if (settings.localStorageSupported) {
|
||||||
await clearLocalKeys(workspace.id);
|
deletions.push(clearLocalKeys(workspace.id));
|
||||||
}
|
}
|
||||||
setLocalKeys([]);
|
const results = await Promise.allSettled(deletions);
|
||||||
await load();
|
await load();
|
||||||
|
if (
|
||||||
|
results.some(
|
||||||
|
result => result.status === 'rejected' || result.value === false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error('Some BYOK profiles could not be deleted');
|
||||||
|
}
|
||||||
}, [load, settings, workspace.id, workspaceServer.server]);
|
}, [load, settings, workspace.id, workspaceServer.server]);
|
||||||
|
|
||||||
const deleteKey = useCallback(
|
const deleteKey = useCallback(
|
||||||
async (key: ByokKey) => {
|
async (key: ByokKey) => {
|
||||||
if (key.storage === ByokKeyStorage.local) {
|
if (key.storage === ByokStorage.local) {
|
||||||
await deleteLocalKey(workspace.id, key.id);
|
await deleteLocalKey(workspace.id, key.id);
|
||||||
setLocalKeys(await readLocalKeys(workspace.id));
|
setLocalKeys(await readLocalKeys(workspace.id));
|
||||||
return;
|
return;
|
||||||
@@ -154,14 +168,39 @@ export const WorkspaceByokSetting = () => {
|
|||||||
}) => Promise<unknown>)
|
}) => Promise<unknown>)
|
||||||
| undefined;
|
| undefined;
|
||||||
await gql?.({
|
await gql?.({
|
||||||
query: deleteByokMutation,
|
query: deleteWorkspaceByokProfileMutation,
|
||||||
variables: { workspaceId: workspace.id, id: key.id },
|
variables: { workspaceId: workspace.id, profileId: key.id },
|
||||||
});
|
});
|
||||||
await load();
|
await load();
|
||||||
},
|
},
|
||||||
[load, workspace.id, workspaceServer.server]
|
[load, workspace.id, workspaceServer.server]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const testKey = useCallback(
|
||||||
|
async (key: ByokKey) => {
|
||||||
|
if (key.storage !== ByokStorage.server || !workspaceServer.server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTestingKeyId(key.id);
|
||||||
|
try {
|
||||||
|
await (workspaceServer.server.gql as GqlFn)({
|
||||||
|
query: probeWorkspaceByokProfileMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
profileId: key.id,
|
||||||
|
checks: probeChecks(key.definition.models, false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await load();
|
||||||
|
} finally {
|
||||||
|
setTestingKeyId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[load, workspace.id, workspaceServer.server]
|
||||||
|
);
|
||||||
|
|
||||||
const reorderKey = useCallback(
|
const reorderKey = useCallback(
|
||||||
async (targetKey: ByokKey) => {
|
async (targetKey: ByokKey) => {
|
||||||
if (!draggingKey || draggingKey.id === targetKey.id) {
|
if (!draggingKey || draggingKey.id === targetKey.id) {
|
||||||
@@ -187,38 +226,44 @@ export const WorkspaceByokSetting = () => {
|
|||||||
nextBucket.splice(toIndex, 0, moved);
|
nextBucket.splice(toIndex, 0, moved);
|
||||||
const nextBucketIds = nextBucket.map(key => key.id);
|
const nextBucketIds = nextBucket.map(key => key.id);
|
||||||
|
|
||||||
if (targetKey.storage === ByokKeyStorage.local) {
|
if (targetKey.storage === ByokStorage.local) {
|
||||||
setLocalKeys(await reorderLocalKeys(workspace.id, nextBucketIds));
|
setLocalKeys(await reorderLocalKeys(workspace.id, nextBucketIds));
|
||||||
return;
|
} else if (workspaceServer.server) {
|
||||||
}
|
if (nextBucket.some(key => key.revision === undefined)) {
|
||||||
|
notify.error({
|
||||||
const gql = workspaceServer.server?.gql as
|
title: byokT(t, 'notify.reload-required.title'),
|
||||||
| ((input: {
|
message: byokT(t, 'notify.reload-required.message'),
|
||||||
query: GraphQLQuery;
|
});
|
||||||
variables?: Record<string, unknown>;
|
await load();
|
||||||
}) => Promise<unknown>)
|
return;
|
||||||
| undefined;
|
}
|
||||||
await gql?.({
|
await (workspaceServer.server.gql as GqlFn)({
|
||||||
query: reorderByokMutation,
|
query: reorderWorkspaceByokProfilesMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
storage: ByokKeyStorage.server,
|
profiles: nextBucket.flatMap(key =>
|
||||||
ids: nextBucketIds,
|
key.revision === undefined
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
profileId: key.id,
|
||||||
|
expectedRevision: key.revision,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
await load();
|
||||||
await load();
|
}
|
||||||
},
|
},
|
||||||
[draggingKey, keys, load, t, workspace.id, workspaceServer.server]
|
[draggingKey, keys, load, t, workspace.id, workspaceServer.server]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return (
|
return (
|
||||||
<SettingHeader
|
<SettingHeader title={byokT(t, 'title')} subtitle={byokT(t, 'loading')} />
|
||||||
title={byokT(t, 'title-beta')}
|
|
||||||
subtitle={byokT(t, 'loading')}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +271,7 @@ export const WorkspaceByokSetting = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingHeader
|
<SettingHeader
|
||||||
title={byokT(t, 'title-beta')}
|
title={byokT(t, 'title')}
|
||||||
subtitle={byokT(t, 'subtitle')}
|
subtitle={byokT(t, 'subtitle')}
|
||||||
/>
|
/>
|
||||||
<SettingWrapper>
|
<SettingWrapper>
|
||||||
@@ -237,13 +282,7 @@ export const WorkspaceByokSetting = () => {
|
|||||||
{byokT(t, 'locked.description')}
|
{byokT(t, 'locked.description')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}></div>
|
||||||
{settings.entitlementRequired.map(plan => (
|
|
||||||
<span className={styles.tag} key={plan}>
|
|
||||||
{plan}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
</>
|
</>
|
||||||
@@ -252,21 +291,9 @@ export const WorkspaceByokSetting = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingHeader
|
<SettingHeader title={byokT(t, 'title')} subtitle={byokT(t, 'header')} />
|
||||||
title={byokT(t, 'title-beta')}
|
|
||||||
subtitle={byokT(t, 'header')}
|
|
||||||
/>
|
|
||||||
<SettingWrapper>
|
<SettingWrapper>
|
||||||
<div className={styles.stack}>
|
<div className={styles.stack}>
|
||||||
{settings.hasAiPlan ? (
|
|
||||||
<div className={styles.notice}>
|
|
||||||
<div className={styles.title}>{byokT(t, 'notice.title')}</div>
|
|
||||||
<div className={styles.description}>
|
|
||||||
{byokT(t, 'notice.description')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className={styles.panel} data-testid="workspace-byok-keys">
|
<div className={styles.panel} data-testid="workspace-byok-keys">
|
||||||
<div className={styles.panelHeader}>
|
<div className={styles.panelHeader}>
|
||||||
<div>
|
<div>
|
||||||
@@ -289,6 +316,7 @@ export const WorkspaceByokSetting = () => {
|
|||||||
{keys.length ? (
|
{keys.length ? (
|
||||||
<KeyList
|
<KeyList
|
||||||
keys={keys}
|
keys={keys}
|
||||||
|
testingKeyId={testingKeyId}
|
||||||
onEdit={key => {
|
onEdit={key => {
|
||||||
setEditingKey(key);
|
setEditingKey(key);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@@ -302,6 +330,15 @@ export const WorkspaceByokSetting = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onTest={key => {
|
||||||
|
testKey(key).catch(error => {
|
||||||
|
logByokError('Failed to test BYOK provider', error);
|
||||||
|
notify.error({
|
||||||
|
title: byokT(t, 'notify.test-failed.title'),
|
||||||
|
message: byokT(t, 'notify.operation-failed.message'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
onDragStart={key => {
|
onDragStart={key => {
|
||||||
setDraggingKey({ id: key.id, storage: key.storage });
|
setDraggingKey({ id: key.id, storage: key.storage });
|
||||||
}}
|
}}
|
||||||
@@ -326,7 +363,7 @@ export const WorkspaceByokSetting = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CoveragePanel keys={keys} settings={settings} />
|
<CoveragePanel keys={keys} />
|
||||||
|
|
||||||
<UsagePanel
|
<UsagePanel
|
||||||
keys={keys}
|
keys={keys}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DragHandle, IconButton } from '@affine/component';
|
import { Button, DragHandle, IconButton } from '@affine/component';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { DeleteIcon, EditIcon } from '@blocksuite/icons/rc';
|
import { DeleteIcon, EditIcon } from '@blocksuite/icons/rc';
|
||||||
import type { DragEvent } from 'react';
|
import type { DragEvent } from 'react';
|
||||||
@@ -11,19 +11,23 @@ import {
|
|||||||
rowDescription,
|
rowDescription,
|
||||||
storageLabel,
|
storageLabel,
|
||||||
} from './metadata';
|
} from './metadata';
|
||||||
import type { ByokKey } from './types';
|
import { type ByokKey, ByokStorage } from './types';
|
||||||
|
|
||||||
export const KeyList = ({
|
export const KeyList = ({
|
||||||
keys,
|
keys,
|
||||||
|
testingKeyId,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onTest,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
onDrop,
|
onDrop,
|
||||||
}: {
|
}: {
|
||||||
keys: ByokKey[];
|
keys: ByokKey[];
|
||||||
|
testingKeyId: string | null;
|
||||||
onEdit: (key: ByokKey) => void;
|
onEdit: (key: ByokKey) => void;
|
||||||
onDelete: (key: ByokKey) => void;
|
onDelete: (key: ByokKey) => void;
|
||||||
|
onTest: (key: ByokKey) => void;
|
||||||
onDragStart: (key: ByokKey) => void;
|
onDragStart: (key: ByokKey) => void;
|
||||||
onDragEnd: () => void;
|
onDragEnd: () => void;
|
||||||
onDrop: (key: ByokKey) => void;
|
onDrop: (key: ByokKey) => void;
|
||||||
@@ -72,6 +76,18 @@ export const KeyList = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rowActions}>
|
<div className={styles.rowActions}>
|
||||||
|
{key.storage === ByokStorage.server ? (
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
disabled={testingKeyId !== null}
|
||||||
|
onClick={() => onTest(key)}
|
||||||
|
>
|
||||||
|
{byokT(
|
||||||
|
t,
|
||||||
|
testingKeyId === key.id ? 'action.testing' : 'action.test'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<IconButton
|
<IconButton
|
||||||
size="20"
|
size="20"
|
||||||
title={byokT(t, 'action.edit')}
|
title={byokT(t, 'action.edit')}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { apis } from '@affine/electron-api';
|
import { apis } from '@affine/electron-api';
|
||||||
import { ByokKeyStorage, ByokKeyTestStatus } from '@affine/graphql';
|
|
||||||
|
|
||||||
import { capabilitiesFor } from './metadata';
|
import { capabilitiesFor } from './metadata';
|
||||||
import type { ByokKey, LocalByokKeyInput, LocalByokPublicKey } from './types';
|
import {
|
||||||
|
type ByokKey,
|
||||||
|
ByokStorage,
|
||||||
|
type LocalByokKeyInput,
|
||||||
|
type LocalByokPublicKey,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
function byokStorageApi() {
|
function byokStorageApi() {
|
||||||
return BUILD_CONFIG.isElectron ? apis?.byokStorage : undefined;
|
return BUILD_CONFIG.isElectron ? apis?.byokStorage : undefined;
|
||||||
@@ -26,14 +30,12 @@ function toLocalByokKey(key: LocalByokPublicKey): ByokKey {
|
|||||||
provider: key.provider,
|
provider: key.provider,
|
||||||
name: key.name,
|
name: key.name,
|
||||||
description: key.description ?? null,
|
description: key.description ?? null,
|
||||||
storage: ByokKeyStorage.local,
|
storage: ByokStorage.local,
|
||||||
configured: key.configured ?? true,
|
configured: key.configured ?? true,
|
||||||
enabled: key.enabled ?? true,
|
enabled: key.enabled ?? true,
|
||||||
endpoint: key.endpoint ?? null,
|
|
||||||
endpointEditable: key.endpointEditable ?? false,
|
|
||||||
sortOrder: key.sortOrder ?? 0,
|
sortOrder: key.sortOrder ?? 0,
|
||||||
capabilities: capabilitiesFor(key.provider, ByokKeyStorage.local),
|
definition: key.definition,
|
||||||
testStatus: key.testStatus ?? ByokKeyTestStatus.passed,
|
capabilities: capabilitiesFor(key),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
|
||||||
|
|
||||||
import { endpointHintKey, shouldShowEndpoint } from './metadata';
|
|
||||||
|
|
||||||
describe('endpointHintKey', () => {
|
|
||||||
test.each([
|
|
||||||
[false, false, 'endpoint.custom-disabled'],
|
|
||||||
[true, false, 'endpoint.private-disabled'],
|
|
||||||
[true, true, null],
|
|
||||||
] as const)(
|
|
||||||
'maps custom=%s private=%s to %s',
|
|
||||||
(customEndpointSupported, privateEndpointSupported, expected) => {
|
|
||||||
expect(
|
|
||||||
endpointHintKey(customEndpointSupported, privateEndpointSupported)
|
|
||||||
).toBe(expected);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('shouldShowEndpoint', () => {
|
|
||||||
test.each([
|
|
||||||
[true, false, true],
|
|
||||||
[true, true, true],
|
|
||||||
[false, true, true],
|
|
||||||
[false, false, false],
|
|
||||||
])(
|
|
||||||
'self-hosted %s with custom endpoint support %s returns %s',
|
|
||||||
(isSelfHosted, customEndpointSupported, expected) => {
|
|
||||||
expect(shouldShowEndpoint(isSelfHosted, customEndpointSupported)).toBe(
|
|
||||||
expected
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ByokKeyStorage, ByokProvider } from '@affine/graphql';
|
import { ByokProvider } from '@affine/graphql';
|
||||||
import type { I18nInstance } from '@affine/i18n';
|
import type { I18nInstance } from '@affine/i18n';
|
||||||
|
|
||||||
import type { ByokKey, ByokStorage } from './types';
|
import { type ByokKey, ByokStorage } from './types';
|
||||||
|
|
||||||
export function byokT(
|
export function byokT(
|
||||||
t: I18nInstance,
|
t: I18nInstance,
|
||||||
@@ -19,7 +19,7 @@ export const providerLabels: Record<ByokProvider, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function storageLabel(t: I18nInstance, storage: ByokStorage) {
|
export function storageLabel(t: I18nInstance, storage: ByokStorage) {
|
||||||
return storage === ByokKeyStorage.local
|
return storage === ByokStorage.local
|
||||||
? byokT(t, 'storage.local')
|
? byokT(t, 'storage.local')
|
||||||
: byokT(t, 'storage.server');
|
: byokT(t, 'storage.server');
|
||||||
}
|
}
|
||||||
@@ -44,26 +44,20 @@ export function shouldShowEndpoint(
|
|||||||
return isSelfHosted || customEndpointSupported;
|
return isSelfHosted || customEndpointSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function capabilitiesFor(provider: ByokProvider, storage: ByokStorage) {
|
export function capabilitiesFor(key: Pick<ByokKey, 'definition'>) {
|
||||||
switch (provider) {
|
const capabilities = key.definition.models.flatMap(model =>
|
||||||
case ByokProvider.openai:
|
model.enabled ? model.capabilities : []
|
||||||
return ['Text', 'Image input', 'Actions', 'Image generate'];
|
);
|
||||||
case ByokProvider.anthropic:
|
const labels = new Set<string>();
|
||||||
return ['Text', 'Image input'];
|
for (const capability of capabilities) {
|
||||||
case ByokProvider.gemini:
|
if (capability.output.includes('text')) labels.add('Text');
|
||||||
return storage === ByokKeyStorage.server
|
if (capability.input.includes('image')) labels.add('Image input');
|
||||||
? [
|
if (capability.output.includes('image')) labels.add('Image generate');
|
||||||
'Text',
|
if (capability.features.includes('tools')) labels.add('Actions');
|
||||||
'Image input',
|
if (capability.input.includes('audio')) labels.add('Transcript');
|
||||||
'Actions',
|
if (capability.output.includes('embedding')) labels.add('Indexing');
|
||||||
'Image generate',
|
|
||||||
'Transcript',
|
|
||||||
'Indexing',
|
|
||||||
]
|
|
||||||
: ['Text', 'Image input', 'Actions', 'Image generate'];
|
|
||||||
case ByokProvider.fal:
|
|
||||||
return ['Image generate'];
|
|
||||||
}
|
}
|
||||||
|
return [...labels];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function capabilityLabel(t: I18nInstance, capability: string) {
|
export function capabilityLabel(t: I18nInstance, capability: string) {
|
||||||
@@ -121,7 +115,7 @@ export const capabilityRows = [
|
|||||||
icon: 'transcript',
|
icon: 'transcript',
|
||||||
providers: [ByokProvider.gemini],
|
providers: [ByokProvider.gemini],
|
||||||
coverageCapabilities: ['Transcript'],
|
coverageCapabilities: ['Transcript'],
|
||||||
storage: ByokKeyStorage.server,
|
storage: ByokStorage.server,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
titleKey: 'feature.workspace-indexing.title',
|
titleKey: 'feature.workspace-indexing.title',
|
||||||
@@ -130,7 +124,7 @@ export const capabilityRows = [
|
|||||||
icon: 'indexing',
|
icon: 'indexing',
|
||||||
providers: [ByokProvider.gemini],
|
providers: [ByokProvider.gemini],
|
||||||
coverageCapabilities: ['Indexing'],
|
coverageCapabilities: ['Indexing'],
|
||||||
storage: ByokKeyStorage.server,
|
storage: ByokStorage.server,
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -145,16 +139,13 @@ function formatDate(value?: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function rowDescription(t: I18nInstance, key: ByokKey) {
|
export function rowDescription(t: I18nInstance, key: ByokKey) {
|
||||||
const failed = formatDate(key.lastErrorAt);
|
const tested = formatDate(key.validation?.connection.testedAt);
|
||||||
const used = formatDate(key.lastUsedAt);
|
const activity =
|
||||||
const today = formatDate(new Date().toISOString());
|
key.validation?.connection.kind === 'failed'
|
||||||
const activity = failed
|
? byokT(t, 'row.activity.failed', { date: tested ?? '' })
|
||||||
? byokT(t, 'row.activity.failed', { date: failed })
|
: key.validation?.connection.kind === 'verified'
|
||||||
: used
|
? byokT(t, 'status.key-verified')
|
||||||
? used === today
|
: byokT(t, 'row.activity.unused');
|
||||||
? byokT(t, 'row.activity.used-today')
|
|
||||||
: byokT(t, 'row.activity.used', { date: used })
|
|
||||||
: byokT(t, 'row.activity.unused');
|
|
||||||
|
|
||||||
return [storageLabel(t, key.storage), activity, key.description]
|
return [storageLabel(t, key.storage), activity, key.description]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import { Button, Checkbox, Input, Modal } from '@affine/component';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.css';
|
||||||
|
import { byokT } from './metadata';
|
||||||
|
import {
|
||||||
|
capabilitiesForUseCases,
|
||||||
|
type catalogModels,
|
||||||
|
type ModelDeclaration,
|
||||||
|
modelUseCases,
|
||||||
|
type UseCase,
|
||||||
|
useCases,
|
||||||
|
} from './model-utils';
|
||||||
|
|
||||||
|
export const ModelEditorModal = ({
|
||||||
|
open,
|
||||||
|
customEndpoint,
|
||||||
|
catalog,
|
||||||
|
models,
|
||||||
|
editingModel,
|
||||||
|
onOpenChange,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
customEndpoint: boolean;
|
||||||
|
catalog: ReturnType<typeof catalogModels>;
|
||||||
|
models: ModelDeclaration[];
|
||||||
|
editingModel: ModelDeclaration | null;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSubmit: (models: ModelDeclaration[]) => void;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
const [modelId, setModelId] = useState('');
|
||||||
|
const [selectedUseCases, setSelectedUseCases] = useState<UseCase[]>(['chat']);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
setSearch('');
|
||||||
|
setSelectedIds([]);
|
||||||
|
setModelId(editingModel?.modelId ?? '');
|
||||||
|
setSelectedUseCases(editingModel ? modelUseCases(editingModel) : ['chat']);
|
||||||
|
}, [editingModel, open]);
|
||||||
|
|
||||||
|
const availableCatalog = useMemo(() => {
|
||||||
|
return catalog
|
||||||
|
.filter(item => !models.some(model => model.modelId === item.modelId))
|
||||||
|
.sort(
|
||||||
|
(left, right) => Number(right.recommended) - Number(left.recommended)
|
||||||
|
);
|
||||||
|
}, [catalog, models]);
|
||||||
|
const available = useMemo(() => {
|
||||||
|
const query = search.trim().toLocaleLowerCase();
|
||||||
|
return availableCatalog.filter(
|
||||||
|
item =>
|
||||||
|
!query ||
|
||||||
|
item.displayName.toLocaleLowerCase().includes(query) ||
|
||||||
|
item.modelId.toLocaleLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}, [availableCatalog, search]);
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
if (customEndpoint) {
|
||||||
|
onSubmit([
|
||||||
|
{
|
||||||
|
modelId: modelId.trim(),
|
||||||
|
enabled: editingModel?.enabled ?? true,
|
||||||
|
capabilities: capabilitiesForUseCases(editingModel, selectedUseCases),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
onSubmit(
|
||||||
|
selectedIds.flatMap(id => {
|
||||||
|
const model = catalog.find(item => item.modelId === id);
|
||||||
|
return model
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
modelId: model.modelId,
|
||||||
|
enabled: true,
|
||||||
|
capabilities: model.capabilities,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizedModelId = modelId.trim();
|
||||||
|
const duplicateModelId = models.some(
|
||||||
|
model => model !== editingModel && model.modelId === normalizedModelId
|
||||||
|
);
|
||||||
|
const valid = customEndpoint
|
||||||
|
? !!normalizedModelId && !duplicateModelId && selectedUseCases.length > 0
|
||||||
|
: selectedIds.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
width={customEndpoint ? 520 : 560}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
descriptionClassName={styles.modelModalDescription}
|
||||||
|
title={byokT(
|
||||||
|
t,
|
||||||
|
editingModel
|
||||||
|
? 'modal.edit-model-title'
|
||||||
|
: customEndpoint
|
||||||
|
? 'modal.add-custom-model-title'
|
||||||
|
: 'modal.add-model-title'
|
||||||
|
)}
|
||||||
|
description={byokT(
|
||||||
|
t,
|
||||||
|
customEndpoint
|
||||||
|
? 'modal.custom-model-description'
|
||||||
|
: 'modal.catalog-model-description'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{customEndpoint ? (
|
||||||
|
<div className={styles.modelModalBody}>
|
||||||
|
<label className={styles.field}>
|
||||||
|
<span className={styles.modelFieldLabel}>
|
||||||
|
{byokT(t, 'field.model-id')}
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
value={modelId}
|
||||||
|
onChange={setModelId}
|
||||||
|
placeholder={byokT(t, 'placeholder.model-id')}
|
||||||
|
/>
|
||||||
|
{duplicateModelId ? (
|
||||||
|
<span className={styles.error}>
|
||||||
|
{byokT(t, 'model.duplicate-id')}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</label>
|
||||||
|
<fieldset className={styles.modelCapabilities}>
|
||||||
|
<legend className={styles.modelFieldLabel}>
|
||||||
|
{byokT(t, 'model.use-this-for')}
|
||||||
|
</legend>
|
||||||
|
<div className={styles.useCaseGrid}>
|
||||||
|
{useCases.map(useCase => (
|
||||||
|
<Checkbox
|
||||||
|
className={styles.modelUseCase}
|
||||||
|
labelClassName={styles.modelUseCaseLabel}
|
||||||
|
key={useCase.id}
|
||||||
|
name={`byok-model-use-${useCase.id}`}
|
||||||
|
label={byokT(t, useCase.labelKey)}
|
||||||
|
checked={selectedUseCases.includes(useCase.id)}
|
||||||
|
onChange={(_, checked) =>
|
||||||
|
setSelectedUseCases(
|
||||||
|
checked
|
||||||
|
? [...selectedUseCases, useCase.id]
|
||||||
|
: selectedUseCases.filter(item => item !== useCase.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.modelModalBody}>
|
||||||
|
{availableCatalog.length > 6 ? (
|
||||||
|
<Input
|
||||||
|
className={styles.modelSearch}
|
||||||
|
size="large"
|
||||||
|
value={search}
|
||||||
|
onChange={setSearch}
|
||||||
|
placeholder={byokT(t, 'placeholder.search-models')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.catalogChoices}>
|
||||||
|
{available.length ? (
|
||||||
|
available.map(model => {
|
||||||
|
const modelUses = modelUseCases({
|
||||||
|
modelId: model.modelId,
|
||||||
|
enabled: true,
|
||||||
|
capabilities: model.capabilities,
|
||||||
|
});
|
||||||
|
const visibleUses = modelUses.slice(0, 3).flatMap(useCase => {
|
||||||
|
const item = useCases.find(item => item.id === useCase);
|
||||||
|
return item ? [byokT(t, item.labelKey)] : [];
|
||||||
|
});
|
||||||
|
if (modelUses.length > 3) {
|
||||||
|
visibleUses.push(`+${modelUses.length - 3}`);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={styles.catalogChoice}
|
||||||
|
data-selected={selectedIds.includes(model.modelId)}
|
||||||
|
key={model.modelId}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
className={styles.modelCheckbox}
|
||||||
|
aria-label={model.displayName}
|
||||||
|
checked={selectedIds.includes(model.modelId)}
|
||||||
|
onChange={(_, checked) =>
|
||||||
|
setSelectedIds(
|
||||||
|
checked
|
||||||
|
? [...selectedIds, model.modelId]
|
||||||
|
: selectedIds.filter(id => id !== model.modelId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className={styles.catalogModelCopy}>
|
||||||
|
<span className={styles.catalogModelTitle}>
|
||||||
|
<strong>{model.displayName}</strong>
|
||||||
|
{model.recommended ? (
|
||||||
|
<span className={styles.recommended}>
|
||||||
|
{byokT(t, 'model.recommended')}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={styles.catalogModelMeta}
|
||||||
|
title={[model.modelId, ...visibleUses].join(' · ')}
|
||||||
|
>
|
||||||
|
{[model.modelId, ...visibleUses].join(' · ')}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className={styles.modelEmpty}>
|
||||||
|
{byokT(
|
||||||
|
t,
|
||||||
|
search ? 'models.no-search-results' : 'models.all-added'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.modelModalActions}>
|
||||||
|
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
|
{byokT(t, 'action.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" disabled={!valid} onClick={submit}>
|
||||||
|
{byokT(
|
||||||
|
t,
|
||||||
|
editingModel
|
||||||
|
? 'action.save-model'
|
||||||
|
: customEndpoint
|
||||||
|
? 'action.add-model'
|
||||||
|
: 'action.add-selected-models',
|
||||||
|
{ count: selectedIds.length }
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DragHandle,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Switch,
|
||||||
|
} from '@affine/component';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.css';
|
||||||
|
import { byokT } from './metadata';
|
||||||
|
import { ModelEditorModal } from './model-editor-modal';
|
||||||
|
import {
|
||||||
|
type catalogModels,
|
||||||
|
type ModelDeclaration,
|
||||||
|
modelUseCases,
|
||||||
|
useCases,
|
||||||
|
} from './model-utils';
|
||||||
|
import type { ByokKey } from './types';
|
||||||
|
|
||||||
|
export const ModelSelector = ({
|
||||||
|
customEndpoint,
|
||||||
|
catalog,
|
||||||
|
models,
|
||||||
|
validation,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
customEndpoint: boolean;
|
||||||
|
catalog: ReturnType<typeof catalogModels>;
|
||||||
|
models: ModelDeclaration[];
|
||||||
|
validation?: ByokKey['validation'];
|
||||||
|
onChange: (models: ModelDeclaration[]) => void;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||||
|
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const update = (index: number, model: ModelDeclaration) => {
|
||||||
|
onChange(models.map((current, i) => (i === index ? model : current)));
|
||||||
|
};
|
||||||
|
const move = (index: number, offset: number) => {
|
||||||
|
const target = index + offset;
|
||||||
|
if (target < 0 || target >= models.length) return;
|
||||||
|
const next = [...models];
|
||||||
|
[next[index], next[target]] = [next[target], next[index]];
|
||||||
|
onChange(next);
|
||||||
|
};
|
||||||
|
const drop = (targetIndex: number) => {
|
||||||
|
if (draggingIndex === null || draggingIndex === targetIndex) return;
|
||||||
|
const next = [...models];
|
||||||
|
const [dragged] = next.splice(draggingIndex, 1);
|
||||||
|
next.splice(targetIndex, 0, dragged);
|
||||||
|
onChange(next);
|
||||||
|
setDraggingIndex(null);
|
||||||
|
};
|
||||||
|
const evidence = (modelId: string) => {
|
||||||
|
const checks = validation?.models.find(
|
||||||
|
model => model.modelId === modelId
|
||||||
|
)?.checks;
|
||||||
|
if (!checks?.length) return byokT(t, 'model.status.not-tested');
|
||||||
|
const verified = checks.filter(
|
||||||
|
check => check.status.kind === 'verified'
|
||||||
|
).length;
|
||||||
|
if (verified === checks.length) return byokT(t, 'model.status.verified');
|
||||||
|
if (verified === 0) return byokT(t, 'model.status.failed');
|
||||||
|
return byokT(t, 'model.status.partially-verified', {
|
||||||
|
verified,
|
||||||
|
total: checks.length,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.modelToolbar}>
|
||||||
|
<span className={styles.description}>
|
||||||
|
{byokT(t, 'models.description.order')}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingIndex(null);
|
||||||
|
setEditorOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{byokT(t, 'action.add-model')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{models.length ? (
|
||||||
|
<ol className={styles.selectedModels}>
|
||||||
|
{models.map((model, index) => {
|
||||||
|
const catalogModel = catalog.find(
|
||||||
|
item => item.modelId === model.modelId
|
||||||
|
);
|
||||||
|
const selected = modelUseCases(model);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className={`${styles.selectedModel} ${
|
||||||
|
model.enabled ? '' : styles.selectedModelDisabled
|
||||||
|
}`}
|
||||||
|
key={model.modelId}
|
||||||
|
onDragOver={event => event.preventDefault()}
|
||||||
|
onDrop={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
drop(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.modelDragHandle}
|
||||||
|
draggable
|
||||||
|
title={byokT(t, 'action.reorder')}
|
||||||
|
onDragStart={() => setDraggingIndex(index)}
|
||||||
|
onDragEnd={() => setDraggingIndex(null)}
|
||||||
|
>
|
||||||
|
<DragHandle dragging={draggingIndex === index} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.modelCopy}>
|
||||||
|
<strong>{catalogModel?.displayName ?? model.modelId}</strong>
|
||||||
|
{catalogModel?.displayName ? (
|
||||||
|
<small>{model.modelId}</small>
|
||||||
|
) : null}
|
||||||
|
<span className={styles.tags}>
|
||||||
|
{selected.slice(0, 3).map(useCase => {
|
||||||
|
const item = useCases.find(item => item.id === useCase);
|
||||||
|
return item ? (
|
||||||
|
<span className={styles.tag} key={useCase}>
|
||||||
|
{byokT(t, item.labelKey)}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
{selected.length > 3 ? (
|
||||||
|
<span className={styles.tag}>+{selected.length - 3}</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className={styles.modelStatus}>
|
||||||
|
{model.enabled
|
||||||
|
? evidence(model.modelId)
|
||||||
|
: byokT(t, 'model.status.disabled')}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={model.enabled}
|
||||||
|
aria-label={byokT(
|
||||||
|
t,
|
||||||
|
model.enabled
|
||||||
|
? 'action.disable-model'
|
||||||
|
: 'action.enable-model',
|
||||||
|
{ model: catalogModel?.displayName ?? model.modelId }
|
||||||
|
)}
|
||||||
|
onChange={enabled => update(index, { ...model, enabled })}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
items={
|
||||||
|
<>
|
||||||
|
{customEndpoint ? (
|
||||||
|
<MenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
setEditingIndex(index);
|
||||||
|
setEditorOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{byokT(t, 'action.edit')}
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
|
<MenuItem
|
||||||
|
disabled={index === 0}
|
||||||
|
onSelect={() => move(index, -1)}
|
||||||
|
>
|
||||||
|
{byokT(t, 'action.move-up')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
disabled={index === models.length - 1}
|
||||||
|
onSelect={() => move(index, 1)}
|
||||||
|
>
|
||||||
|
{byokT(t, 'action.move-down')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
type="danger"
|
||||||
|
onSelect={() =>
|
||||||
|
onChange(models.filter((_, i) => i !== index))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{byokT(t, 'action.remove')}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="20"
|
||||||
|
title={byokT(t, 'action.model-options', {
|
||||||
|
model: catalogModel?.displayName ?? model.modelId,
|
||||||
|
})}
|
||||||
|
icon={<MoreHorizontalIcon />}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
) : (
|
||||||
|
<div className={styles.modelEmpty}>{byokT(t, 'models.empty')}</div>
|
||||||
|
)}
|
||||||
|
<ModelEditorModal
|
||||||
|
open={editorOpen}
|
||||||
|
customEndpoint={customEndpoint}
|
||||||
|
catalog={catalog}
|
||||||
|
models={models}
|
||||||
|
editingModel={editingIndex === null ? null : models[editingIndex]}
|
||||||
|
onOpenChange={open => {
|
||||||
|
setEditorOpen(open);
|
||||||
|
if (!open) setEditingIndex(null);
|
||||||
|
}}
|
||||||
|
onSubmit={next => {
|
||||||
|
if (editingIndex === null) {
|
||||||
|
onChange([...models, ...next]);
|
||||||
|
} else if (next[0]) {
|
||||||
|
update(editingIndex, next[0]);
|
||||||
|
}
|
||||||
|
setEditingIndex(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
capabilitiesForUseCases,
|
||||||
|
type ModelDeclaration,
|
||||||
|
modelUseCases,
|
||||||
|
} from './model-utils';
|
||||||
|
|
||||||
|
describe('BYOK model capabilities', () => {
|
||||||
|
test('maps richer catalog capabilities by minimum requirements', () => {
|
||||||
|
const model: ModelDeclaration = {
|
||||||
|
modelId: 'multimodal-tools',
|
||||||
|
enabled: true,
|
||||||
|
capabilities: [
|
||||||
|
{
|
||||||
|
input: ['text', 'image'],
|
||||||
|
output: ['text'],
|
||||||
|
features: ['tools'],
|
||||||
|
attachmentKinds: ['image'],
|
||||||
|
attachmentSources: ['url', 'data', 'bytes', 'file_handle'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(modelUseCases(model)).toEqual(['chat', 'actions', 'vision']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves a rich capability when its represented uses stay selected', () => {
|
||||||
|
const capability = {
|
||||||
|
input: ['text', 'image'],
|
||||||
|
output: ['text'],
|
||||||
|
features: ['tools'],
|
||||||
|
attachmentKinds: ['image'],
|
||||||
|
attachmentSources: ['url', 'data', 'bytes', 'file_handle'],
|
||||||
|
};
|
||||||
|
const model: ModelDeclaration = {
|
||||||
|
modelId: 'multimodal-tools',
|
||||||
|
enabled: true,
|
||||||
|
capabilities: [capability],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
capabilitiesForUseCases(model, ['chat', 'actions', 'vision'])
|
||||||
|
).toEqual([capability]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import type { ByokProvider } from '@affine/graphql';
|
||||||
|
|
||||||
|
import type { ByokDefinition, ByokSettings } from './types';
|
||||||
|
|
||||||
|
export type ModelDeclaration = ByokDefinition['models'][number];
|
||||||
|
type Capability = ModelDeclaration['capabilities'][number];
|
||||||
|
export type UseCase =
|
||||||
|
| 'chat'
|
||||||
|
| 'actions'
|
||||||
|
| 'structured'
|
||||||
|
| 'vision'
|
||||||
|
| 'image'
|
||||||
|
| 'transcript'
|
||||||
|
| 'embedding'
|
||||||
|
| 'rerank';
|
||||||
|
|
||||||
|
export const useCases: { id: UseCase; labelKey: string }[] = [
|
||||||
|
{ id: 'chat', labelKey: 'model.use.chat' },
|
||||||
|
{ id: 'actions', labelKey: 'model.use.actions' },
|
||||||
|
{ id: 'structured', labelKey: 'model.use.structured' },
|
||||||
|
{ id: 'vision', labelKey: 'model.use.vision' },
|
||||||
|
{ id: 'image', labelKey: 'model.use.image' },
|
||||||
|
{ id: 'transcript', labelKey: 'model.use.transcript' },
|
||||||
|
{ id: 'embedding', labelKey: 'model.use.embedding' },
|
||||||
|
{ id: 'rerank', labelKey: 'model.use.rerank' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function capabilityForUseCase(useCase: UseCase): Capability {
|
||||||
|
switch (useCase) {
|
||||||
|
case 'actions':
|
||||||
|
return modelCapability(['text'], ['text'], ['tools']);
|
||||||
|
case 'structured':
|
||||||
|
return modelCapability(['text'], ['structured']);
|
||||||
|
case 'vision':
|
||||||
|
return modelCapability(
|
||||||
|
['text', 'image'],
|
||||||
|
['text'],
|
||||||
|
[],
|
||||||
|
['image'],
|
||||||
|
['url', 'data', 'bytes', 'file_handle']
|
||||||
|
);
|
||||||
|
case 'image':
|
||||||
|
return modelCapability(['text'], ['image']);
|
||||||
|
case 'transcript':
|
||||||
|
return modelCapability(
|
||||||
|
['audio'],
|
||||||
|
['structured'],
|
||||||
|
[],
|
||||||
|
['audio'],
|
||||||
|
['url', 'data', 'bytes', 'file_handle']
|
||||||
|
);
|
||||||
|
case 'embedding':
|
||||||
|
return modelCapability(['text'], ['embedding']);
|
||||||
|
case 'rerank':
|
||||||
|
return modelCapability(['text'], ['rerank']);
|
||||||
|
default:
|
||||||
|
return modelCapability(['text'], ['text']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelCapability(
|
||||||
|
input: string[],
|
||||||
|
output: string[],
|
||||||
|
features: string[] = [],
|
||||||
|
attachmentKinds: string[] = [],
|
||||||
|
attachmentSources: string[] = []
|
||||||
|
): Capability {
|
||||||
|
return { input, output, features, attachmentKinds, attachmentSources };
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesCapability(value: Capability, useCase: UseCase) {
|
||||||
|
const expected = capabilityForUseCase(useCase);
|
||||||
|
const fields = [
|
||||||
|
'input',
|
||||||
|
'output',
|
||||||
|
'features',
|
||||||
|
'attachmentKinds',
|
||||||
|
'attachmentSources',
|
||||||
|
] as const;
|
||||||
|
return fields.every(field =>
|
||||||
|
expected[field].every(item => value[field].includes(item))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modelUseCases(model: ModelDeclaration) {
|
||||||
|
return useCases
|
||||||
|
.filter(({ id }) =>
|
||||||
|
model.capabilities.some(item => matchesCapability(item, id))
|
||||||
|
)
|
||||||
|
.map(({ id }) => id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function capabilitiesForUseCases(
|
||||||
|
model: ModelDeclaration | null,
|
||||||
|
selectedUseCases: UseCase[]
|
||||||
|
) {
|
||||||
|
const selected = new Set(selectedUseCases);
|
||||||
|
const capabilities = (model?.capabilities ?? []).filter(capability => {
|
||||||
|
const represented = useCases
|
||||||
|
.map(({ id }) => id)
|
||||||
|
.filter(useCase => matchesCapability(capability, useCase));
|
||||||
|
return (
|
||||||
|
represented.length > 0 &&
|
||||||
|
represented.every(useCase => selected.has(useCase))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const useCase of selectedUseCases) {
|
||||||
|
if (
|
||||||
|
!capabilities.some(capability => matchesCapability(capability, useCase))
|
||||||
|
) {
|
||||||
|
capabilities.push(capabilityForUseCase(useCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function probeChecks(models: ModelDeclaration[], includeImage: boolean) {
|
||||||
|
return models
|
||||||
|
.filter(model => model.enabled)
|
||||||
|
.flatMap(model =>
|
||||||
|
modelUseCases(model)
|
||||||
|
.filter(
|
||||||
|
useCase =>
|
||||||
|
!['vision', 'transcript'].includes(useCase) &&
|
||||||
|
(useCase !== 'image' || includeImage)
|
||||||
|
)
|
||||||
|
.map(useCase => ({
|
||||||
|
modelId: model.modelId,
|
||||||
|
operation: useCase === 'actions' ? 'tools' : useCase,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function catalogModels(settings: ByokSettings, provider: ByokProvider) {
|
||||||
|
return (
|
||||||
|
settings.catalog.providers.find(item => item.provider === provider)
|
||||||
|
?.models ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultModels(settings: ByokSettings, provider: ByokProvider) {
|
||||||
|
const catalog = catalogModels(settings, provider);
|
||||||
|
const selected = catalog.filter(model => model.recommended);
|
||||||
|
return (selected.length ? selected : catalog.slice(0, 1)).map(model => ({
|
||||||
|
modelId: model.modelId,
|
||||||
|
enabled: true,
|
||||||
|
capabilities: model.capabilities,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1,76 +1,64 @@
|
|||||||
import {
|
import {
|
||||||
type ByokKeyStorage,
|
|
||||||
type ByokKeyTestStatus,
|
|
||||||
type ByokProvider,
|
type ByokProvider,
|
||||||
type GraphQLQuery,
|
type GraphQLQuery,
|
||||||
type QueryOptions,
|
type QueryOptions,
|
||||||
type QueryResponse,
|
type QueryResponse,
|
||||||
|
type WorkspaceByokSettingsQuery,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
|
|
||||||
export type ByokStorage = ByokKeyStorage;
|
export const ByokStorage = {
|
||||||
|
server: 'server',
|
||||||
|
local: 'local',
|
||||||
|
} as const;
|
||||||
|
export type ByokStorage = (typeof ByokStorage)[keyof typeof ByokStorage];
|
||||||
|
|
||||||
export type ByokKey = {
|
export type ByokDefinition =
|
||||||
|
WorkspaceByokSettingsQuery['workspace']['byokSettings']['profiles'][number]['definition'];
|
||||||
|
|
||||||
|
type ByokKeyBase = {
|
||||||
id: string;
|
id: string;
|
||||||
provider: ByokProvider;
|
provider: ByokProvider;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
storage: ByokStorage;
|
|
||||||
configured: boolean;
|
configured: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
endpoint?: string | null;
|
|
||||||
endpointEditable: boolean;
|
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
definition: ByokDefinition;
|
||||||
capabilities: string[];
|
capabilities: string[];
|
||||||
testStatus: ByokKeyTestStatus;
|
validation?: WorkspaceByokSettingsQuery['workspace']['byokSettings']['profiles'][number]['validation'];
|
||||||
disabledReason?: string | null;
|
|
||||||
lastTestedAt?: string | null;
|
|
||||||
lastTestError?: string | null;
|
|
||||||
lastUsedAt?: string | null;
|
|
||||||
lastErrorAt?: string | null;
|
|
||||||
lastError?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ByokKey = ByokKeyBase &
|
||||||
|
(
|
||||||
|
| { storage: typeof ByokStorage.server; revision: number }
|
||||||
|
| { storage: typeof ByokStorage.local; revision?: never }
|
||||||
|
);
|
||||||
|
|
||||||
export type LocalByokKeyInput = Pick<
|
export type LocalByokKeyInput = Pick<
|
||||||
ByokKey,
|
ByokKey,
|
||||||
| 'id'
|
| 'id'
|
||||||
| 'provider'
|
| 'provider'
|
||||||
| 'name'
|
| 'name'
|
||||||
| 'description'
|
| 'description'
|
||||||
| 'endpoint'
|
|
||||||
| 'sortOrder'
|
| 'sortOrder'
|
||||||
| 'enabled'
|
| 'enabled'
|
||||||
|
| 'definition'
|
||||||
|
> & { credential: string };
|
||||||
|
|
||||||
|
export type ByokSettings = Omit<
|
||||||
|
WorkspaceByokSettingsQuery['workspace']['byokSettings'],
|
||||||
|
'profiles'
|
||||||
> & {
|
> & {
|
||||||
apiKey: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ByokSettings = {
|
|
||||||
workspaceId: string;
|
|
||||||
entitled: boolean;
|
|
||||||
serverEntitled: boolean;
|
|
||||||
localEntitled: boolean;
|
|
||||||
entitlementRequired: string[];
|
|
||||||
keys: ByokKey[];
|
keys: ByokKey[];
|
||||||
allowedProviders: ByokProvider[];
|
|
||||||
localStorageSupported: boolean;
|
localStorageSupported: boolean;
|
||||||
customEndpointSupported: boolean;
|
|
||||||
privateEndpointSupported: boolean;
|
|
||||||
hasAiPlan: boolean;
|
|
||||||
warnings: Array<{
|
|
||||||
featureKind: string;
|
|
||||||
reason: string;
|
|
||||||
requiredProviders: ByokProvider[];
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ByokUsagePoint = {
|
export type ByokUsagePoint =
|
||||||
date: string;
|
WorkspaceByokSettingsQuery['workspace']['byokUsage'][number];
|
||||||
featureKind: string;
|
|
||||||
totalTokens: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ByokTestResult = {
|
export type ByokTestResult = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
status: ByokKey['testStatus'];
|
status: string;
|
||||||
message?: string | null;
|
message?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,15 +66,6 @@ export type GqlFn = <Query extends GraphQLQuery>(
|
|||||||
input: QueryOptions<Query>
|
input: QueryOptions<Query>
|
||||||
) => Promise<QueryResponse<Query>>;
|
) => Promise<QueryResponse<Query>>;
|
||||||
|
|
||||||
export type LocalByokPublicKey = {
|
export type LocalByokPublicKey = Omit<LocalByokKeyInput, 'credential'> & {
|
||||||
id: string;
|
|
||||||
provider: ByokProvider;
|
|
||||||
name: string;
|
|
||||||
description?: string | null;
|
|
||||||
endpoint?: string | null;
|
|
||||||
endpointEditable?: boolean;
|
|
||||||
sortOrder?: number | null;
|
|
||||||
enabled?: boolean | null;
|
|
||||||
configured?: boolean;
|
configured?: boolean;
|
||||||
testStatus?: ByokKey['testStatus'];
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import { useAISpecs } from '@affine/core/components/hooks/affine/use-ai-specs';
|
|||||||
import { useAISubscribe } from '@affine/core/components/hooks/affine/use-ai-subscribe';
|
import { useAISubscribe } from '@affine/core/components/hooks/affine/use-ai-subscribe';
|
||||||
import {
|
import {
|
||||||
AIDraftService,
|
AIDraftService,
|
||||||
|
AIModelService,
|
||||||
AIToolsConfigService,
|
AIToolsConfigService,
|
||||||
} from '@affine/core/modules/ai-button';
|
} from '@affine/core/modules/ai-button';
|
||||||
import { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
|
||||||
import {
|
import {
|
||||||
EventSourceService,
|
EventSourceService,
|
||||||
GraphQLService,
|
GraphQLService,
|
||||||
@@ -197,9 +197,9 @@ export const Component = () => {
|
|||||||
content.notificationService = notificationService;
|
content.notificationService = notificationService;
|
||||||
content.aiDraftService = framework.get(AIDraftService);
|
content.aiDraftService = framework.get(AIDraftService);
|
||||||
content.aiToolsConfigService = framework.get(AIToolsConfigService);
|
content.aiToolsConfigService = framework.get(AIToolsConfigService);
|
||||||
|
content.aiModelService = framework.get(AIModelService);
|
||||||
content.serverService = framework.get(ServerService);
|
content.serverService = framework.get(ServerService);
|
||||||
content.subscriptionService = framework.get(SubscriptionService);
|
content.subscriptionService = framework.get(SubscriptionService);
|
||||||
content.aiModelService = framework.get(AIModelService);
|
|
||||||
content.onAISubscribe = handleAISubscribe;
|
content.onAISubscribe = handleAISubscribe;
|
||||||
content.onOpenDoc = onOpenDoc;
|
content.onOpenDoc = onOpenDoc;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import { useAISpecs } from '@affine/core/components/hooks/affine/use-ai-specs';
|
|||||||
import { useAISubscribe } from '@affine/core/components/hooks/affine/use-ai-subscribe';
|
import { useAISubscribe } from '@affine/core/components/hooks/affine/use-ai-subscribe';
|
||||||
import {
|
import {
|
||||||
AIDraftService,
|
AIDraftService,
|
||||||
|
AIModelService,
|
||||||
AIToolsConfigService,
|
AIToolsConfigService,
|
||||||
} from '@affine/core/modules/ai-button';
|
} from '@affine/core/modules/ai-button';
|
||||||
import { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
|
||||||
import {
|
import {
|
||||||
EventSourceService,
|
EventSourceService,
|
||||||
GraphQLService,
|
GraphQLService,
|
||||||
@@ -283,9 +283,9 @@ export const EditorChatPanel = ({
|
|||||||
content.notificationService = notificationService;
|
content.notificationService = notificationService;
|
||||||
content.aiDraftService = framework.get(AIDraftService);
|
content.aiDraftService = framework.get(AIDraftService);
|
||||||
content.aiToolsConfigService = framework.get(AIToolsConfigService);
|
content.aiToolsConfigService = framework.get(AIToolsConfigService);
|
||||||
|
content.aiModelService = framework.get(AIModelService);
|
||||||
content.peekViewService = framework.get(PeekViewService);
|
content.peekViewService = framework.get(PeekViewService);
|
||||||
content.subscriptionService = framework.get(SubscriptionService);
|
content.subscriptionService = framework.get(SubscriptionService);
|
||||||
content.aiModelService = framework.get(AIModelService);
|
|
||||||
content.onAISubscribe = handleAISubscribe;
|
content.onAISubscribe = handleAISubscribe;
|
||||||
content.width = sidebarWidthSignal;
|
content.width = sidebarWidthSignal;
|
||||||
content.onOpenDoc = (docId: string, sessionId?: string) => {
|
content.onOpenDoc = (docId: string, sessionId?: string) => {
|
||||||
@@ -399,8 +399,8 @@ export const EditorChatPanel = ({
|
|||||||
.notificationService=${notificationService}
|
.notificationService=${notificationService}
|
||||||
.affineWorkspaceDialogService=${framework.get(WorkspaceDialogService)}
|
.affineWorkspaceDialogService=${framework.get(WorkspaceDialogService)}
|
||||||
.aiToolsConfigService=${framework.get(AIToolsConfigService)}
|
.aiToolsConfigService=${framework.get(AIToolsConfigService)}
|
||||||
.subscriptionService=${framework.get(SubscriptionService)}
|
|
||||||
.aiModelService=${framework.get(AIModelService)}
|
.aiModelService=${framework.get(AIModelService)}
|
||||||
|
.subscriptionService=${framework.get(SubscriptionService)}
|
||||||
></playground-content>
|
></playground-content>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export { AIButtonProvider } from './provider/ai-button';
|
export { AIButtonProvider } from './provider/ai-button';
|
||||||
export { AIButtonService } from './services/ai-button';
|
export { AIButtonService } from './services/ai-button';
|
||||||
export { AIDraftService } from './services/ai-draft';
|
export { AIDraftService } from './services/ai-draft';
|
||||||
|
export { AIModelService } from './services/models';
|
||||||
export {
|
export {
|
||||||
type AIToolsConfig,
|
type AIToolsConfig,
|
||||||
AIToolsConfigService,
|
AIToolsConfigService,
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Framework } from '@toeverything/infra';
|
||||||
|
import { EMPTY } from 'rxjs';
|
||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { AIModelService } from './models';
|
||||||
|
|
||||||
|
describe('AIModelService', () => {
|
||||||
|
test('clears the previous model while a new scope is loading', async () => {
|
||||||
|
let resolveSecond: ((value: unknown) => void) | undefined;
|
||||||
|
const gql = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
currentUser: {
|
||||||
|
copilot: {
|
||||||
|
routeOptions: {
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
id: 'model-a',
|
||||||
|
displayName: 'Model A',
|
||||||
|
available: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(
|
||||||
|
() =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
resolveSecond = resolve;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const stored = new Map<string, unknown>();
|
||||||
|
const framework = new Framework();
|
||||||
|
framework.service(
|
||||||
|
AIModelService,
|
||||||
|
() =>
|
||||||
|
new AIModelService(
|
||||||
|
{
|
||||||
|
globalState: {
|
||||||
|
get: (key: string) => stored.get(key),
|
||||||
|
set: (key: string, value: unknown) => stored.set(key, value),
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
{ gql } as never,
|
||||||
|
{
|
||||||
|
subscription: { ai$: EMPTY },
|
||||||
|
} as never
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const service = framework.provider().get(AIModelService);
|
||||||
|
|
||||||
|
service.setScope('workspace-a', 'route-a');
|
||||||
|
await vi.waitFor(() => expect(service.models.value).toHaveLength(1));
|
||||||
|
service.setModel('model-a');
|
||||||
|
|
||||||
|
service.setScope('workspace-b', 'route-b');
|
||||||
|
expect(service.models.value).toEqual([]);
|
||||||
|
expect(service.modelId.value).toBeUndefined();
|
||||||
|
service.setModel('model-a');
|
||||||
|
expect(stored.has('AIManagedRouteTarget:workspace-b:route-b')).toBe(false);
|
||||||
|
|
||||||
|
resolveSecond?.({
|
||||||
|
currentUser: { copilot: { routeOptions: { choices: [] } } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,112 +1,109 @@
|
|||||||
import { getPromptModelsQuery, SubscriptionStatus } from '@affine/graphql';
|
import { getCopilotRouteOptionsQuery } from '@affine/graphql';
|
||||||
import {
|
|
||||||
createSignalFromObservable,
|
|
||||||
type Signal,
|
|
||||||
} from '@blocksuite/affine/shared/utils';
|
|
||||||
import { signal } from '@preact/signals-core';
|
import { signal } from '@preact/signals-core';
|
||||||
import { LiveData, Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { GraphQLService, SubscriptionService } from '../../cloud';
|
import type { GraphQLService, SubscriptionService } from '../../cloud';
|
||||||
import type { GlobalStateService } from '../../storage';
|
import type { GlobalStateService } from '../../storage';
|
||||||
|
|
||||||
const AI_MODEL_ID_KEY = 'AIModelId';
|
const ROUTE_TARGET_KEY = 'AIManagedRouteTarget';
|
||||||
|
|
||||||
export interface AIModel {
|
export interface AIModel {
|
||||||
name: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
version: string;
|
name: string;
|
||||||
category: string;
|
category: string;
|
||||||
isPro: boolean;
|
version: string;
|
||||||
isDefault: boolean;
|
available: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AIModelService extends Service {
|
export class AIModelService extends Service {
|
||||||
modelId: Signal<string | undefined>;
|
readonly modelId = signal<string | undefined>(undefined);
|
||||||
|
readonly models = signal<AIModel[]>([]);
|
||||||
|
|
||||||
models: Signal<AIModel[]> = signal([]);
|
private workspaceId: string | undefined;
|
||||||
|
private routeId: string | undefined;
|
||||||
private readonly modelId$ = LiveData.from(
|
private requestId = 0;
|
||||||
this.globalStateService.globalState.watch<string>(AI_MODEL_ID_KEY),
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly globalStateService: GlobalStateService,
|
private readonly globalStateService: GlobalStateService,
|
||||||
private readonly gqlService: GraphQLService,
|
private readonly gqlService: GraphQLService,
|
||||||
private readonly subscriptionService: SubscriptionService
|
subscriptionService: SubscriptionService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
const subscription = subscriptionService.subscription.ai$.subscribe(() => {
|
||||||
const { signal: modelId, cleanup } = createSignalFromObservable<
|
if (this.workspaceId && this.routeId) {
|
||||||
string | undefined
|
this.load(this.workspaceId, this.routeId).catch(console.error);
|
||||||
>(this.modelId$, undefined);
|
}
|
||||||
this.modelId = modelId;
|
|
||||||
this.disposables.push(cleanup);
|
|
||||||
|
|
||||||
this.init().catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
});
|
||||||
|
this.disposables.push(() => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
resetModel = () => {
|
setScope(workspaceId: string, routeId: string) {
|
||||||
this.globalStateService.globalState.set(AI_MODEL_ID_KEY, undefined);
|
if (workspaceId === this.workspaceId && routeId === this.routeId) return;
|
||||||
};
|
this.workspaceId = workspaceId;
|
||||||
|
this.routeId = routeId;
|
||||||
|
this.models.value = [];
|
||||||
|
this.modelId.value = undefined;
|
||||||
|
this.load(workspaceId, routeId).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
setModel = (modelId: string) => {
|
resetModel() {
|
||||||
const isSubscribed =
|
this.setModel(undefined);
|
||||||
this.subscriptionService.subscription.ai$.value?.status ===
|
}
|
||||||
SubscriptionStatus.Active;
|
|
||||||
const model = this.models.value.find(model => model.id === modelId);
|
setModel(modelId: string | undefined) {
|
||||||
if (!isSubscribed && model?.isPro) {
|
if (!this.workspaceId || !this.routeId) return;
|
||||||
|
if (
|
||||||
|
modelId &&
|
||||||
|
!this.models.value.find(model => model.id === modelId)?.available
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.globalStateService.globalState.set(AI_MODEL_ID_KEY, modelId);
|
this.modelId.value = modelId;
|
||||||
};
|
this.globalStateService.globalState.set(
|
||||||
|
this.storageKey(this.workspaceId, this.routeId),
|
||||||
private readonly init = async () => {
|
modelId
|
||||||
await this.initModels();
|
|
||||||
|
|
||||||
// subscribe to ai purchase status
|
|
||||||
const sub = this.subscriptionService.subscription.ai$.subscribe(
|
|
||||||
subscription => {
|
|
||||||
const isSubscribed = subscription?.status === SubscriptionStatus.Active;
|
|
||||||
const model = this.models.value.find(
|
|
||||||
model => model.id === this.modelId.value
|
|
||||||
);
|
|
||||||
if (!isSubscribed && model?.isPro) {
|
|
||||||
this.resetModel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
this.disposables.push(() => sub.unsubscribe());
|
}
|
||||||
};
|
|
||||||
|
|
||||||
private readonly initModels = async (prompt?: string) => {
|
private async load(workspaceId: string, routeId: string) {
|
||||||
const promptName = prompt || 'Chat With AFFiNE AI';
|
const requestId = ++this.requestId;
|
||||||
const models = await this.getModelsByPrompt(promptName);
|
const result = await this.gqlService.gql({
|
||||||
if (models) {
|
query: getCopilotRouteOptionsQuery,
|
||||||
const { defaultModel, optionalModels, proModels } = models;
|
variables: { promptName: routeId },
|
||||||
this.models.value = optionalModels.map(model => {
|
});
|
||||||
const [category] = model.name.split(' ');
|
if (requestId !== this.requestId) return;
|
||||||
const version = model.name.slice(category.length + 1);
|
const options = result.currentUser?.copilot?.routeOptions;
|
||||||
return {
|
if (!options) {
|
||||||
name: model.name,
|
this.models.value = [];
|
||||||
id: model.id,
|
this.modelId.value = undefined;
|
||||||
version,
|
return;
|
||||||
category,
|
|
||||||
isPro: proModels.some(proModel => proModel.id === model.id),
|
|
||||||
isDefault: model.id === defaultModel,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
this.models.value = options.choices.map(choice => {
|
||||||
|
const [category] = choice.displayName.split(' ');
|
||||||
|
return {
|
||||||
|
id: choice.id,
|
||||||
|
name: choice.displayName,
|
||||||
|
category,
|
||||||
|
version: choice.displayName.slice(category.length + 1),
|
||||||
|
available: choice.available,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const selected = this.globalStateService.globalState.get<string>(
|
||||||
|
this.storageKey(workspaceId, routeId)
|
||||||
|
);
|
||||||
|
const selectedAvailable = this.models.value.some(
|
||||||
|
model => model.id === selected && model.available
|
||||||
|
);
|
||||||
|
this.modelId.value = selectedAvailable ? selected : undefined;
|
||||||
|
if (selected && !selectedAvailable) {
|
||||||
|
this.globalStateService.globalState.set(
|
||||||
|
this.storageKey(workspaceId, routeId),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private readonly getModelsByPrompt = async (promptName: string) => {
|
private storageKey(workspaceId: string, routeId: string) {
|
||||||
return this.gqlService
|
return `${ROUTE_TARGET_KEY}:${workspaceId}:${routeId}`;
|
||||||
.gql({
|
}
|
||||||
query: getPromptModelsQuery,
|
|
||||||
variables: { promptName },
|
|
||||||
})
|
|
||||||
.then(res => res.currentUser?.copilot?.models);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,10 +69,7 @@ describe('AudioTranscriptionJobStore transcript task API', () => {
|
|||||||
.mockResolvedValueOnce({ submitTranscriptTask: { id: 'task-1' } })
|
.mockResolvedValueOnce({ submitTranscriptTask: { id: 'task-1' } })
|
||||||
.mockResolvedValueOnce({ retryTranscriptTask: { id: 'task-2' } })
|
.mockResolvedValueOnce({ retryTranscriptTask: { id: 'task-2' } })
|
||||||
.mockResolvedValueOnce({ settleTranscriptTask: { id: 'task-2' } });
|
.mockResolvedValueOnce({ settleTranscriptTask: { id: 'task-2' } });
|
||||||
const store = createStore(gql, async () => ({
|
const store = createStore(gql, async () => ({ files: [file] }));
|
||||||
files: [file],
|
|
||||||
input: { strategy: 'gemini' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
await store.submitTranscriptTask();
|
await store.submitTranscriptTask();
|
||||||
await store.retryTranscriptTask('task-1');
|
await store.retryTranscriptTask('task-1');
|
||||||
@@ -87,7 +84,7 @@ describe('AudioTranscriptionJobStore transcript task API', () => {
|
|||||||
workspaceId: 'workspace-1',
|
workspaceId: 'workspace-1',
|
||||||
blobId: 'blob-1',
|
blobId: 'blob-1',
|
||||||
blobs: [file],
|
blobs: [file],
|
||||||
input: { strategy: 'gemini' },
|
input: undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { useAIChatConfig } from '@affine/core/components/hooks/affine/use-ai-cha
|
|||||||
import { useAISubscribe } from '@affine/core/components/hooks/affine/use-ai-subscribe';
|
import { useAISubscribe } from '@affine/core/components/hooks/affine/use-ai-subscribe';
|
||||||
import {
|
import {
|
||||||
AIDraftService,
|
AIDraftService,
|
||||||
|
AIModelService,
|
||||||
AIToolsConfigService,
|
AIToolsConfigService,
|
||||||
} from '@affine/core/modules/ai-button';
|
} from '@affine/core/modules/ai-button';
|
||||||
import { AIModelService } from '@affine/core/modules/ai-button/services/models';
|
|
||||||
import { ServerService, SubscriptionService } from '@affine/core/modules/cloud';
|
import { ServerService, SubscriptionService } from '@affine/core/modules/cloud';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
@@ -36,8 +36,8 @@ export const AIChatBlockPeekView = ({
|
|||||||
const affineWorkspaceDialogService = framework.get(WorkspaceDialogService);
|
const affineWorkspaceDialogService = framework.get(WorkspaceDialogService);
|
||||||
const aiDraftService = framework.get(AIDraftService);
|
const aiDraftService = framework.get(AIDraftService);
|
||||||
const aiToolsConfigService = framework.get(AIToolsConfigService);
|
const aiToolsConfigService = framework.get(AIToolsConfigService);
|
||||||
const subscriptionService = framework.get(SubscriptionService);
|
|
||||||
const aiModelService = framework.get(AIModelService);
|
const aiModelService = framework.get(AIModelService);
|
||||||
|
const subscriptionService = framework.get(SubscriptionService);
|
||||||
const handleAISubscribe = useAISubscribe();
|
const handleAISubscribe = useAISubscribe();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
@@ -52,8 +52,8 @@ export const AIChatBlockPeekView = ({
|
|||||||
affineWorkspaceDialogService,
|
affineWorkspaceDialogService,
|
||||||
aiDraftService,
|
aiDraftService,
|
||||||
aiToolsConfigService,
|
aiToolsConfigService,
|
||||||
subscriptionService,
|
|
||||||
aiModelService,
|
aiModelService,
|
||||||
|
subscriptionService,
|
||||||
handleAISubscribe
|
handleAISubscribe
|
||||||
);
|
);
|
||||||
return toReactNode(template);
|
return toReactNode(template);
|
||||||
@@ -68,8 +68,8 @@ export const AIChatBlockPeekView = ({
|
|||||||
affineWorkspaceDialogService,
|
affineWorkspaceDialogService,
|
||||||
aiDraftService,
|
aiDraftService,
|
||||||
aiToolsConfigService,
|
aiToolsConfigService,
|
||||||
subscriptionService,
|
|
||||||
aiModelService,
|
aiModelService,
|
||||||
|
subscriptionService,
|
||||||
handleAISubscribe,
|
handleAISubscribe,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
{
|
{
|
||||||
"ar": 92,
|
"ar": 90,
|
||||||
"ca": 89,
|
"ca": 88,
|
||||||
"da": 4,
|
"da": 3,
|
||||||
"de": 98,
|
"de": 98,
|
||||||
"el-GR": 88,
|
"el-GR": 86,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-AR": 88,
|
"es-AR": 87,
|
||||||
"es-CL": 89,
|
"es-CL": 88,
|
||||||
"es": 88,
|
"es": 86,
|
||||||
"fa": 88,
|
"fa": 86,
|
||||||
"fr": 92,
|
"fr": 90,
|
||||||
"hi": 1,
|
"hi": 1,
|
||||||
"it": 89,
|
"it": 88,
|
||||||
"ja": 88,
|
"ja": 86,
|
||||||
"kk": 96,
|
"kk": 93,
|
||||||
"ko": 89,
|
"ko": 87,
|
||||||
"nb-NO": 44,
|
"nb-NO": 43,
|
||||||
"pl": 89,
|
"pl": 88,
|
||||||
"pt-BR": 88,
|
"pt-BR": 86,
|
||||||
"ru": 90,
|
"ru": 88,
|
||||||
"sv-SE": 88,
|
"sv-SE": 87,
|
||||||
"tr": 96,
|
"tr": 93,
|
||||||
"uk": 88,
|
"uk": 86,
|
||||||
"ur": 96,
|
"ur": 93,
|
||||||
"zh-Hans": 100,
|
"zh-Hans": 97,
|
||||||
"zh-Hant": 90
|
"zh-Hant": 88
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5925,10 +5925,6 @@ export function useAFFiNEI18N(): {
|
|||||||
/**
|
/**
|
||||||
* `AI BYOK (Beta)`
|
* `AI BYOK (Beta)`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.title-beta"](): string;
|
|
||||||
/**
|
|
||||||
* `AI BYOK`
|
|
||||||
*/
|
|
||||||
["com.affine.settings.workspace.byok.title"](): string;
|
["com.affine.settings.workspace.byok.title"](): string;
|
||||||
/**
|
/**
|
||||||
* `Loading provider keys.`
|
* `Loading provider keys.`
|
||||||
@@ -5950,14 +5946,6 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Upgrade this workspace to add provider keys and route AFFiNE AI through your own OpenAI, Anthropic, Gemini, or FAL account.`
|
* `Upgrade this workspace to add provider keys and route AFFiNE AI through your own OpenAI, Anthropic, Gemini, or FAL account.`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.locked.description"](): string;
|
["com.affine.settings.workspace.byok.locked.description"](): string;
|
||||||
/**
|
|
||||||
* `AI plan stays available`
|
|
||||||
*/
|
|
||||||
["com.affine.settings.workspace.byok.notice.title"](): string;
|
|
||||||
/**
|
|
||||||
* `Local keys on this device are tried first. Workspace server keys follow, then AFFiNE AI plan routes when quota is available.`
|
|
||||||
*/
|
|
||||||
["com.affine.settings.workspace.byok.notice.description"](): string;
|
|
||||||
/**
|
/**
|
||||||
* `Provider keys`
|
* `Provider keys`
|
||||||
*/
|
*/
|
||||||
@@ -5983,13 +5971,25 @@ export function useAFFiNEI18N(): {
|
|||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.storage.server"](): string;
|
["com.affine.settings.workspace.byok.storage.server"](): string;
|
||||||
/**
|
/**
|
||||||
* `Local (this device)`
|
* `Available to workspace members.`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.storage.local-this-device"](): string;
|
["com.affine.settings.workspace.byok.storage.server.description"](): string;
|
||||||
/**
|
/**
|
||||||
* `Local (Desktop only)`
|
* `Kept in this desktop device’s secure storage.`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.storage.local-desktop-only"](): string;
|
["com.affine.settings.workspace.byok.storage.local.description"](): string;
|
||||||
|
/**
|
||||||
|
* `Available in the AFFiNE desktop app.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.storage.local.desktop-only"](): string;
|
||||||
|
/**
|
||||||
|
* `Secure local storage is not available on this device.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.storage.local.unavailable"](): string;
|
||||||
|
/**
|
||||||
|
* `Testing sends this key to the workspace server for this request only; it is not stored there.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.storage.local.test-disclosure"](): string;
|
||||||
/**
|
/**
|
||||||
* `Disabled after failure`
|
* `Disabled after failure`
|
||||||
*/
|
*/
|
||||||
@@ -5998,10 +5998,6 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Key verified`
|
* `Key verified`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.status.key-verified"](): string;
|
["com.affine.settings.workspace.byok.status.key-verified"](): string;
|
||||||
/**
|
|
||||||
* `Key test failed`
|
|
||||||
*/
|
|
||||||
["com.affine.settings.workspace.byok.status.key-test-failed"](): string;
|
|
||||||
/**
|
/**
|
||||||
* `Text`
|
* `Text`
|
||||||
*/
|
*/
|
||||||
@@ -6032,16 +6028,6 @@ export function useAFFiNEI18N(): {
|
|||||||
["com.affine.settings.workspace.byok.row.activity.failed"](options: {
|
["com.affine.settings.workspace.byok.row.activity.failed"](options: {
|
||||||
readonly date: string;
|
readonly date: string;
|
||||||
}): string;
|
}): string;
|
||||||
/**
|
|
||||||
* `used {{date}}`
|
|
||||||
*/
|
|
||||||
["com.affine.settings.workspace.byok.row.activity.used"](options: {
|
|
||||||
readonly date: string;
|
|
||||||
}): string;
|
|
||||||
/**
|
|
||||||
* `used today`
|
|
||||||
*/
|
|
||||||
["com.affine.settings.workspace.byok.row.activity.used-today"](): string;
|
|
||||||
/**
|
/**
|
||||||
* `not used yet`
|
* `not used yet`
|
||||||
*/
|
*/
|
||||||
@@ -6113,17 +6099,69 @@ export function useAFFiNEI18N(): {
|
|||||||
readonly count: string;
|
readonly count: string;
|
||||||
}): string;
|
}): string;
|
||||||
/**
|
/**
|
||||||
* `Add provider key`
|
* `Connect AI provider`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.modal.add-title"](): string;
|
["com.affine.settings.workspace.byok.modal.connect-title"](): string;
|
||||||
/**
|
/**
|
||||||
* `Edit provider key`
|
* `Manage provider`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.modal.edit-title"](): string;
|
["com.affine.settings.workspace.byok.modal.manage-title"](): string;
|
||||||
/**
|
/**
|
||||||
* `Re-enter the API key and test it before saving changes.`
|
* `Choose where the key is stored, then select the models AFFiNE may use.`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.modal.description"](): string;
|
["com.affine.settings.workspace.byok.modal.connect-description"](): string;
|
||||||
|
/**
|
||||||
|
* `Add models`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.modal.add-model-title"](): string;
|
||||||
|
/**
|
||||||
|
* `Add custom model`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.modal.add-custom-model-title"](): string;
|
||||||
|
/**
|
||||||
|
* `Edit model`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.modal.edit-model-title"](): string;
|
||||||
|
/**
|
||||||
|
* `Choose one or more models this key may use.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.modal.catalog-model-description"](): string;
|
||||||
|
/**
|
||||||
|
* `Enter the endpoint model ID and choose its uses.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.modal.custom-model-description"](): string;
|
||||||
|
/**
|
||||||
|
* `Connection`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.section.connection"](): string;
|
||||||
|
/**
|
||||||
|
* `Models`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.section.models"](): string;
|
||||||
|
/**
|
||||||
|
* `Advanced details`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.section.advanced"](): string;
|
||||||
|
/**
|
||||||
|
* `Models this key may use.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.models.description.selected"](): string;
|
||||||
|
/**
|
||||||
|
* `Among compatible models, enabled models are tried from top to bottom.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.models.description.order"](): string;
|
||||||
|
/**
|
||||||
|
* `No models added yet.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.models.empty"](): string;
|
||||||
|
/**
|
||||||
|
* `All available models have been added.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.models.all-added"](): string;
|
||||||
|
/**
|
||||||
|
* `No matching models.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.models.no-search-results"](): string;
|
||||||
/**
|
/**
|
||||||
* `Provider`
|
* `Provider`
|
||||||
*/
|
*/
|
||||||
@@ -6137,9 +6175,9 @@ export function useAFFiNEI18N(): {
|
|||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.field.description"](): string;
|
["com.affine.settings.workspace.byok.field.description"](): string;
|
||||||
/**
|
/**
|
||||||
* `Key storage`
|
* `Provider enabled`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.field.storage"](): string;
|
["com.affine.settings.workspace.byok.field.provider-enabled"](): string;
|
||||||
/**
|
/**
|
||||||
* `API key`
|
* `API key`
|
||||||
*/
|
*/
|
||||||
@@ -6148,6 +6186,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Endpoint`
|
* `Endpoint`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.field.endpoint"](): string;
|
["com.affine.settings.workspace.byok.field.endpoint"](): string;
|
||||||
|
/**
|
||||||
|
* `Model ID`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.field.model-id"](): string;
|
||||||
/**
|
/**
|
||||||
* `Custom endpoints are disabled by the server administrator. In Self-hosted Admin, enable copilot.byok.allowCustomEndpoint.`
|
* `Custom endpoints are disabled by the server administrator. In Self-hosted Admin, enable copilot.byok.allowCustomEndpoint.`
|
||||||
*/
|
*/
|
||||||
@@ -6157,29 +6199,176 @@ export function useAFFiNEI18N(): {
|
|||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.endpoint.private-disabled"](): string;
|
["com.affine.settings.workspace.byok.endpoint.private-disabled"](): string;
|
||||||
/**
|
/**
|
||||||
* `Primary`
|
* `Leave blank to keep the current key`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.placeholder.key-name"](): string;
|
["com.affine.settings.workspace.byok.placeholder.keep-current-key"](): string;
|
||||||
/**
|
/**
|
||||||
* `Workspace fallback key`
|
* `Model ID`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.placeholder.description"](): string;
|
["com.affine.settings.workspace.byok.placeholder.model-id"](): string;
|
||||||
|
/**
|
||||||
|
* `Search models…`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.placeholder.search-models"](): string;
|
||||||
|
/**
|
||||||
|
* `Use a custom API-compatible endpoint`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.endpoint.use-custom"](): string;
|
||||||
|
/**
|
||||||
|
* `Include an image generation request when testing (provider charges may apply)`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.probe.include-image"](): string;
|
||||||
|
/**
|
||||||
|
* `Connection verified`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.probe.verified"](): string;
|
||||||
|
/**
|
||||||
|
* `Connection failed`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.probe.failed"](): string;
|
||||||
|
/**
|
||||||
|
* `Recommended`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.recommended"](): string;
|
||||||
|
/**
|
||||||
|
* `This model has already been added.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.duplicate-id"](): string;
|
||||||
|
/**
|
||||||
|
* `Not tested`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.status.not-tested"](): string;
|
||||||
|
/**
|
||||||
|
* `Verified`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.status.verified"](): string;
|
||||||
|
/**
|
||||||
|
* `Failed`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.status.failed"](): string;
|
||||||
|
/**
|
||||||
|
* `Partially verified · {{verified}} of {{total}}`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.status.partially-verified"](options: Readonly<{
|
||||||
|
verified: string;
|
||||||
|
total: string;
|
||||||
|
}>): string;
|
||||||
|
/**
|
||||||
|
* `Disabled`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.status.disabled"](): string;
|
||||||
|
/**
|
||||||
|
* `Use this model for`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.use-this-for"](): string;
|
||||||
|
/**
|
||||||
|
* `Chat & writing`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.use.chat"](): string;
|
||||||
|
/**
|
||||||
|
* `Actions`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.use.actions"](): string;
|
||||||
|
/**
|
||||||
|
* `Structured output`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.use.structured"](): string;
|
||||||
|
/**
|
||||||
|
* `Image understanding`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.use.vision"](): string;
|
||||||
|
/**
|
||||||
|
* `Image generation`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.use.image"](): string;
|
||||||
|
/**
|
||||||
|
* `Transcription`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.use.transcript"](): string;
|
||||||
|
/**
|
||||||
|
* `Workspace indexing`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.use.embedding"](): string;
|
||||||
|
/**
|
||||||
|
* `Search reranking`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.model.use.rerank"](): string;
|
||||||
/**
|
/**
|
||||||
* `Add key`
|
* `Add key`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.action.add-key"](): string;
|
["com.affine.settings.workspace.byok.action.add-key"](): string;
|
||||||
/**
|
/**
|
||||||
* `Test key`
|
* `Test connection`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.action.test-key"](): string;
|
["com.affine.settings.workspace.byok.action.test-connection"](): string;
|
||||||
|
/**
|
||||||
|
* `Test`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.test"](): string;
|
||||||
|
/**
|
||||||
|
* `Testing…`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.testing"](): string;
|
||||||
/**
|
/**
|
||||||
* `Cancel`
|
* `Cancel`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.action.cancel"](): string;
|
["com.affine.settings.workspace.byok.action.cancel"](): string;
|
||||||
/**
|
/**
|
||||||
* `Save key`
|
* `Connect`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.action.save-key"](): string;
|
["com.affine.settings.workspace.byok.action.connect"](): string;
|
||||||
|
/**
|
||||||
|
* `Connecting…`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.connecting"](): string;
|
||||||
|
/**
|
||||||
|
* `Save changes`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.save-changes"](): string;
|
||||||
|
/**
|
||||||
|
* `Add model`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.add-model"](): string;
|
||||||
|
/**
|
||||||
|
* `Add {{count}} models`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.add-selected-models"](options: {
|
||||||
|
readonly count: string;
|
||||||
|
}): string;
|
||||||
|
/**
|
||||||
|
* `Save model`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.save-model"](): string;
|
||||||
|
/**
|
||||||
|
* `Enable {{model}}`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.enable-model"](options: {
|
||||||
|
readonly model: string;
|
||||||
|
}): string;
|
||||||
|
/**
|
||||||
|
* `Disable {{model}}`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.disable-model"](options: {
|
||||||
|
readonly model: string;
|
||||||
|
}): string;
|
||||||
|
/**
|
||||||
|
* `Options for {{model}}`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.model-options"](options: {
|
||||||
|
readonly model: string;
|
||||||
|
}): string;
|
||||||
|
/**
|
||||||
|
* `Move up`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.move-up"](): string;
|
||||||
|
/**
|
||||||
|
* `Move down`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.move-down"](): string;
|
||||||
|
/**
|
||||||
|
* `Remove`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.action.remove"](): string;
|
||||||
/**
|
/**
|
||||||
* `Clear all BYOK keys`
|
* `Clear all BYOK keys`
|
||||||
*/
|
*/
|
||||||
@@ -6228,6 +6417,14 @@ export function useAFFiNEI18N(): {
|
|||||||
* `BYOK keys not cleared`
|
* `BYOK keys not cleared`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.byok.notify.clear-failed.title"](): string;
|
["com.affine.settings.workspace.byok.notify.clear-failed.title"](): string;
|
||||||
|
/**
|
||||||
|
* `BYOK settings changed`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.notify.reload-required.title"](): string;
|
||||||
|
/**
|
||||||
|
* `Reload the settings and try again.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.byok.notify.reload-required.message"](): string;
|
||||||
/**
|
/**
|
||||||
* `Please try again.`
|
* `Please try again.`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1480,25 +1480,19 @@
|
|||||||
"com.affine.settings.workspace": "Workspace",
|
"com.affine.settings.workspace": "Workspace",
|
||||||
"com.affine.settings.workspace.description": "Hier kannst du die Informationen zum aktuellen Workspace anzeigen.",
|
"com.affine.settings.workspace.description": "Hier kannst du die Informationen zum aktuellen Workspace anzeigen.",
|
||||||
"com.affine.settings.workspace.byok.title-beta": "AI BYOK (Beta)",
|
"com.affine.settings.workspace.byok.title-beta": "AI BYOK (Beta)",
|
||||||
"com.affine.settings.workspace.byok.title": "AI BYOK",
|
|
||||||
"com.affine.settings.workspace.byok.loading": "Provider-Schlüssel werden geladen.",
|
"com.affine.settings.workspace.byok.loading": "Provider-Schlüssel werden geladen.",
|
||||||
"com.affine.settings.workspace.byok.subtitle": "Benutze deine eigenen Provider-Schlüssel für diesen Workspace.",
|
"com.affine.settings.workspace.byok.subtitle": "Benutze deine eigenen Provider-Schlüssel für diesen Workspace.",
|
||||||
"com.affine.settings.workspace.byok.header": "Benutze die Workspace-Provider-Schlüssel vor der Verwendung des AFFiNE AI Plans.",
|
"com.affine.settings.workspace.byok.header": "Benutze die Workspace-Provider-Schlüssel vor der Verwendung des AFFiNE AI Plans.",
|
||||||
"com.affine.settings.workspace.byok.locked.title": "Für BYOK ist die Pro-, Team- oder Believer-Version erforderlich",
|
"com.affine.settings.workspace.byok.locked.title": "Für BYOK ist die Pro-, Team- oder Believer-Version erforderlich",
|
||||||
"com.affine.settings.workspace.byok.locked.description": "Upgrade diesen Workspace, um Provider-Schlüssel hinzuzufügen und AFFiNE AI über dein eigenes OpenAI-, Anthropic-, Gemini- oder FAL-Konto zu routen.",
|
"com.affine.settings.workspace.byok.locked.description": "Upgrade diesen Workspace, um Provider-Schlüssel hinzuzufügen und AFFiNE AI über dein eigenes OpenAI-, Anthropic-, Gemini- oder FAL-Konto zu routen.",
|
||||||
"com.affine.settings.workspace.byok.notice.title": "Der AI-Plan bleibt verfügbar",
|
|
||||||
"com.affine.settings.workspace.byok.notice.description": "Zunächst werden lokale Schlüssel auf diesem Gerät geprüft. Anschließend folgen die Schlüssel des Workspace-Servers und schließlich die Routen des AFFiNE AI-Plans, sofern Kontingent verfügbar ist.",
|
|
||||||
"com.affine.settings.workspace.byok.keys.title": "Provider-Schlüssel",
|
"com.affine.settings.workspace.byok.keys.title": "Provider-Schlüssel",
|
||||||
"com.affine.settings.workspace.byok.keys.description": "Die Reihenfolge in der Liste bestimmt die Fallback-Reihenfolge innerhalb jeder Speichergruppe.",
|
"com.affine.settings.workspace.byok.keys.description": "Die Reihenfolge in der Liste bestimmt die Fallback-Reihenfolge innerhalb jeder Speichergruppe.",
|
||||||
"com.affine.settings.workspace.byok.empty.title": "Keine Provider-Schlüssel",
|
"com.affine.settings.workspace.byok.empty.title": "Keine Provider-Schlüssel",
|
||||||
"com.affine.settings.workspace.byok.empty.description": "Füge einen Schlüssel hinzu, um die erste Route für diesen Workspace zu erstellen. Die Zeilen des Providers werden erst angezeigt, wenn ein Schlüssel vorhanden ist.",
|
"com.affine.settings.workspace.byok.empty.description": "Füge einen Schlüssel hinzu, um die erste Route für diesen Workspace zu erstellen. Die Zeilen des Providers werden erst angezeigt, wenn ein Schlüssel vorhanden ist.",
|
||||||
"com.affine.settings.workspace.byok.storage.local": "Lokal",
|
"com.affine.settings.workspace.byok.storage.local": "Lokal",
|
||||||
"com.affine.settings.workspace.byok.storage.server": "Server",
|
"com.affine.settings.workspace.byok.storage.server": "Server",
|
||||||
"com.affine.settings.workspace.byok.storage.local-this-device": "Lokal (dieses Gerät)",
|
|
||||||
"com.affine.settings.workspace.byok.storage.local-desktop-only": "Lokal (nur Desktop)",
|
|
||||||
"com.affine.settings.workspace.byok.status.disabled-after-failure": "Nach Fehler deaktiviert",
|
"com.affine.settings.workspace.byok.status.disabled-after-failure": "Nach Fehler deaktiviert",
|
||||||
"com.affine.settings.workspace.byok.status.key-verified": "Schlüssel verifiziert",
|
"com.affine.settings.workspace.byok.status.key-verified": "Schlüssel verifiziert",
|
||||||
"com.affine.settings.workspace.byok.status.key-test-failed": "Schlüssel-Test fehlgeschlagen",
|
|
||||||
"com.affine.settings.workspace.byok.capability.text": "Text",
|
"com.affine.settings.workspace.byok.capability.text": "Text",
|
||||||
"com.affine.settings.workspace.byok.capability.image-input": "Bildeingabe",
|
"com.affine.settings.workspace.byok.capability.image-input": "Bildeingabe",
|
||||||
"com.affine.settings.workspace.byok.capability.actions": "Aktionen",
|
"com.affine.settings.workspace.byok.capability.actions": "Aktionen",
|
||||||
@@ -1506,8 +1500,6 @@
|
|||||||
"com.affine.settings.workspace.byok.capability.transcript": "Transkript",
|
"com.affine.settings.workspace.byok.capability.transcript": "Transkript",
|
||||||
"com.affine.settings.workspace.byok.capability.indexing": "Indizierung",
|
"com.affine.settings.workspace.byok.capability.indexing": "Indizierung",
|
||||||
"com.affine.settings.workspace.byok.row.activity.failed": "fehlgeschlagen {{date}}",
|
"com.affine.settings.workspace.byok.row.activity.failed": "fehlgeschlagen {{date}}",
|
||||||
"com.affine.settings.workspace.byok.row.activity.used": "verwendet {{date}}",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.used-today": "heute verwendet",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.unused": "noch nicht verwendet",
|
"com.affine.settings.workspace.byok.row.activity.unused": "noch nicht verwendet",
|
||||||
"com.affine.settings.workspace.byok.coverage.title": "Feature-Abdeckung",
|
"com.affine.settings.workspace.byok.coverage.title": "Feature-Abdeckung",
|
||||||
"com.affine.settings.workspace.byok.feature.chat.title": "Schreiben und Chatten",
|
"com.affine.settings.workspace.byok.feature.chat.title": "Schreiben und Chatten",
|
||||||
@@ -1525,23 +1517,15 @@
|
|||||||
"com.affine.settings.workspace.byok.usage.title": "BYOK-Nutzung",
|
"com.affine.settings.workspace.byok.usage.title": "BYOK-Nutzung",
|
||||||
"com.affine.settings.workspace.byok.usage.period": "Letzte 30 Tage",
|
"com.affine.settings.workspace.byok.usage.period": "Letzte 30 Tage",
|
||||||
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} Token",
|
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} Token",
|
||||||
"com.affine.settings.workspace.byok.modal.add-title": "Provider-Schlüssel hinzufügen",
|
|
||||||
"com.affine.settings.workspace.byok.modal.edit-title": "Provider-Schlüssel bearbeiten",
|
|
||||||
"com.affine.settings.workspace.byok.modal.description": "Den API-Key erneut eingeben und vor dem Speichern der Änderungen testen.",
|
|
||||||
"com.affine.settings.workspace.byok.field.provider": "Provider",
|
"com.affine.settings.workspace.byok.field.provider": "Provider",
|
||||||
"com.affine.settings.workspace.byok.field.key-name": "Schlüsselname",
|
"com.affine.settings.workspace.byok.field.key-name": "Schlüsselname",
|
||||||
"com.affine.settings.workspace.byok.field.description": "Beschreibung",
|
"com.affine.settings.workspace.byok.field.description": "Beschreibung",
|
||||||
"com.affine.settings.workspace.byok.field.storage": "Schlüssel-Speicher",
|
|
||||||
"com.affine.settings.workspace.byok.field.api-key": "API-Key",
|
"com.affine.settings.workspace.byok.field.api-key": "API-Key",
|
||||||
"com.affine.settings.workspace.byok.field.endpoint": "Endpunkt",
|
"com.affine.settings.workspace.byok.field.endpoint": "Endpunkt",
|
||||||
"com.affine.settings.workspace.byok.endpoint.custom-disabled": "Benutzerdefinierte Endpunkte wurden vom Serveradministrator deaktiviert. Aktiviere in Self-hosted Admin die Einstellung copilot.byok.allowCustomEndpoint.",
|
"com.affine.settings.workspace.byok.endpoint.custom-disabled": "Benutzerdefinierte Endpunkte wurden vom Serveradministrator deaktiviert. Aktiviere in Self-hosted Admin die Einstellung copilot.byok.allowCustomEndpoint.",
|
||||||
"com.affine.settings.workspace.byok.endpoint.private-disabled": "Für Endpunkte in privaten Netzwerken muss der Serveradministrator zusätzlich die Einstellung copilot.byok.allowPrivateEndpoint aktivieren.",
|
"com.affine.settings.workspace.byok.endpoint.private-disabled": "Für Endpunkte in privaten Netzwerken muss der Serveradministrator zusätzlich die Einstellung copilot.byok.allowPrivateEndpoint aktivieren.",
|
||||||
"com.affine.settings.workspace.byok.placeholder.key-name": "Primär",
|
|
||||||
"com.affine.settings.workspace.byok.placeholder.description": "Workspace-Fallback-Schlüssel",
|
|
||||||
"com.affine.settings.workspace.byok.action.add-key": "Schlüssel hinzufügen",
|
"com.affine.settings.workspace.byok.action.add-key": "Schlüssel hinzufügen",
|
||||||
"com.affine.settings.workspace.byok.action.test-key": "Schlüssel testen",
|
|
||||||
"com.affine.settings.workspace.byok.action.cancel": "Abbrechen",
|
"com.affine.settings.workspace.byok.action.cancel": "Abbrechen",
|
||||||
"com.affine.settings.workspace.byok.action.save-key": "Schlüssel speichern",
|
|
||||||
"com.affine.settings.workspace.byok.action.clear-all": "Alle BYOK-Schlüssel löschen",
|
"com.affine.settings.workspace.byok.action.clear-all": "Alle BYOK-Schlüssel löschen",
|
||||||
"com.affine.settings.workspace.byok.action.reorder": "Umsortieren",
|
"com.affine.settings.workspace.byok.action.reorder": "Umsortieren",
|
||||||
"com.affine.settings.workspace.byok.action.edit": "Bearbeiten",
|
"com.affine.settings.workspace.byok.action.edit": "Bearbeiten",
|
||||||
|
|||||||
@@ -1479,26 +1479,25 @@
|
|||||||
"com.affine.settings.meetings.record.permission-modal.open-setting": "Open System Settings",
|
"com.affine.settings.meetings.record.permission-modal.open-setting": "Open System Settings",
|
||||||
"com.affine.settings.workspace": "Workspace",
|
"com.affine.settings.workspace": "Workspace",
|
||||||
"com.affine.settings.workspace.description": "You can view current workspace's information here.",
|
"com.affine.settings.workspace.description": "You can view current workspace's information here.",
|
||||||
"com.affine.settings.workspace.byok.title-beta": "AI BYOK (Beta)",
|
"com.affine.settings.workspace.byok.title": "AI BYOK (Beta)",
|
||||||
"com.affine.settings.workspace.byok.title": "AI BYOK",
|
|
||||||
"com.affine.settings.workspace.byok.loading": "Loading provider keys.",
|
"com.affine.settings.workspace.byok.loading": "Loading provider keys.",
|
||||||
"com.affine.settings.workspace.byok.subtitle": "Use your own provider keys for this workspace.",
|
"com.affine.settings.workspace.byok.subtitle": "Use your own provider keys for this workspace.",
|
||||||
"com.affine.settings.workspace.byok.header": "Use workspace provider keys before AFFiNE AI plan routes.",
|
"com.affine.settings.workspace.byok.header": "Use workspace provider keys before AFFiNE AI plan routes.",
|
||||||
"com.affine.settings.workspace.byok.locked.title": "BYOK requires Pro, Team, or Believer",
|
"com.affine.settings.workspace.byok.locked.title": "BYOK requires Pro, Team, or Believer",
|
||||||
"com.affine.settings.workspace.byok.locked.description": "Upgrade this workspace to add provider keys and route AFFiNE AI through your own OpenAI, Anthropic, Gemini, or FAL account.",
|
"com.affine.settings.workspace.byok.locked.description": "Upgrade this workspace to add provider keys and route AFFiNE AI through your own OpenAI, Anthropic, Gemini, or FAL account.",
|
||||||
"com.affine.settings.workspace.byok.notice.title": "AI plan stays available",
|
|
||||||
"com.affine.settings.workspace.byok.notice.description": "Local keys on this device are tried first. Workspace server keys follow, then AFFiNE AI plan routes when quota is available.",
|
|
||||||
"com.affine.settings.workspace.byok.keys.title": "Provider keys",
|
"com.affine.settings.workspace.byok.keys.title": "Provider keys",
|
||||||
"com.affine.settings.workspace.byok.keys.description": "List order controls fallback within each storage group.",
|
"com.affine.settings.workspace.byok.keys.description": "List order controls fallback within each storage group.",
|
||||||
"com.affine.settings.workspace.byok.empty.title": "No provider keys",
|
"com.affine.settings.workspace.byok.empty.title": "No provider keys",
|
||||||
"com.affine.settings.workspace.byok.empty.description": "Add a key to create the first route for this workspace. Provider rows are not shown until a key exists.",
|
"com.affine.settings.workspace.byok.empty.description": "Add a key to create the first route for this workspace. Provider rows are not shown until a key exists.",
|
||||||
"com.affine.settings.workspace.byok.storage.local": "Local",
|
"com.affine.settings.workspace.byok.storage.local": "Local",
|
||||||
"com.affine.settings.workspace.byok.storage.server": "Server",
|
"com.affine.settings.workspace.byok.storage.server": "Server",
|
||||||
"com.affine.settings.workspace.byok.storage.local-this-device": "Local (this device)",
|
"com.affine.settings.workspace.byok.storage.server.description": "Available to workspace members.",
|
||||||
"com.affine.settings.workspace.byok.storage.local-desktop-only": "Local (Desktop only)",
|
"com.affine.settings.workspace.byok.storage.local.description": "Kept in this desktop device’s secure storage.",
|
||||||
|
"com.affine.settings.workspace.byok.storage.local.desktop-only": "Available in the AFFiNE desktop app.",
|
||||||
|
"com.affine.settings.workspace.byok.storage.local.unavailable": "Secure local storage is not available on this device.",
|
||||||
|
"com.affine.settings.workspace.byok.storage.local.test-disclosure": "Testing sends this key to the workspace server for this request only; it is not stored there.",
|
||||||
"com.affine.settings.workspace.byok.status.disabled-after-failure": "Disabled after failure",
|
"com.affine.settings.workspace.byok.status.disabled-after-failure": "Disabled after failure",
|
||||||
"com.affine.settings.workspace.byok.status.key-verified": "Key verified",
|
"com.affine.settings.workspace.byok.status.key-verified": "Key verified",
|
||||||
"com.affine.settings.workspace.byok.status.key-test-failed": "Key test failed",
|
|
||||||
"com.affine.settings.workspace.byok.capability.text": "Text",
|
"com.affine.settings.workspace.byok.capability.text": "Text",
|
||||||
"com.affine.settings.workspace.byok.capability.image-input": "Image input",
|
"com.affine.settings.workspace.byok.capability.image-input": "Image input",
|
||||||
"com.affine.settings.workspace.byok.capability.actions": "Actions",
|
"com.affine.settings.workspace.byok.capability.actions": "Actions",
|
||||||
@@ -1506,8 +1505,6 @@
|
|||||||
"com.affine.settings.workspace.byok.capability.transcript": "Transcript",
|
"com.affine.settings.workspace.byok.capability.transcript": "Transcript",
|
||||||
"com.affine.settings.workspace.byok.capability.indexing": "Indexing",
|
"com.affine.settings.workspace.byok.capability.indexing": "Indexing",
|
||||||
"com.affine.settings.workspace.byok.row.activity.failed": "failed {{date}}",
|
"com.affine.settings.workspace.byok.row.activity.failed": "failed {{date}}",
|
||||||
"com.affine.settings.workspace.byok.row.activity.used": "used {{date}}",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.used-today": "used today",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.unused": "not used yet",
|
"com.affine.settings.workspace.byok.row.activity.unused": "not used yet",
|
||||||
"com.affine.settings.workspace.byok.coverage.title": "Feature coverage",
|
"com.affine.settings.workspace.byok.coverage.title": "Feature coverage",
|
||||||
"com.affine.settings.workspace.byok.feature.chat.title": "Writing and chat",
|
"com.affine.settings.workspace.byok.feature.chat.title": "Writing and chat",
|
||||||
@@ -1525,23 +1522,71 @@
|
|||||||
"com.affine.settings.workspace.byok.usage.title": "BYOK usage",
|
"com.affine.settings.workspace.byok.usage.title": "BYOK usage",
|
||||||
"com.affine.settings.workspace.byok.usage.period": "Last 30 days",
|
"com.affine.settings.workspace.byok.usage.period": "Last 30 days",
|
||||||
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} tokens",
|
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} tokens",
|
||||||
"com.affine.settings.workspace.byok.modal.add-title": "Add provider key",
|
"com.affine.settings.workspace.byok.modal.connect-title": "Connect AI provider",
|
||||||
"com.affine.settings.workspace.byok.modal.edit-title": "Edit provider key",
|
"com.affine.settings.workspace.byok.modal.manage-title": "Manage provider",
|
||||||
"com.affine.settings.workspace.byok.modal.description": "Re-enter the API key and test it before saving changes.",
|
"com.affine.settings.workspace.byok.modal.connect-description": "Choose where the key is stored, then select the models AFFiNE may use.",
|
||||||
|
"com.affine.settings.workspace.byok.modal.add-model-title": "Add models",
|
||||||
|
"com.affine.settings.workspace.byok.modal.add-custom-model-title": "Add custom model",
|
||||||
|
"com.affine.settings.workspace.byok.modal.edit-model-title": "Edit model",
|
||||||
|
"com.affine.settings.workspace.byok.modal.catalog-model-description": "Choose one or more models this key may use.",
|
||||||
|
"com.affine.settings.workspace.byok.modal.custom-model-description": "Enter the endpoint model ID and choose its uses.",
|
||||||
|
"com.affine.settings.workspace.byok.section.connection": "Connection",
|
||||||
|
"com.affine.settings.workspace.byok.section.models": "Models",
|
||||||
|
"com.affine.settings.workspace.byok.section.advanced": "Advanced details",
|
||||||
|
"com.affine.settings.workspace.byok.models.description.selected": "Models this key may use.",
|
||||||
|
"com.affine.settings.workspace.byok.models.description.order": "Among compatible models, enabled models are tried from top to bottom.",
|
||||||
|
"com.affine.settings.workspace.byok.models.empty": "No models added yet.",
|
||||||
|
"com.affine.settings.workspace.byok.models.all-added": "All available models have been added.",
|
||||||
|
"com.affine.settings.workspace.byok.models.no-search-results": "No matching models.",
|
||||||
"com.affine.settings.workspace.byok.field.provider": "Provider",
|
"com.affine.settings.workspace.byok.field.provider": "Provider",
|
||||||
"com.affine.settings.workspace.byok.field.key-name": "Key name",
|
"com.affine.settings.workspace.byok.field.key-name": "Key name",
|
||||||
"com.affine.settings.workspace.byok.field.description": "Description",
|
"com.affine.settings.workspace.byok.field.description": "Description",
|
||||||
"com.affine.settings.workspace.byok.field.storage": "Key storage",
|
"com.affine.settings.workspace.byok.field.provider-enabled": "Provider enabled",
|
||||||
"com.affine.settings.workspace.byok.field.api-key": "API key",
|
"com.affine.settings.workspace.byok.field.api-key": "API key",
|
||||||
"com.affine.settings.workspace.byok.field.endpoint": "Endpoint",
|
"com.affine.settings.workspace.byok.field.endpoint": "Endpoint",
|
||||||
|
"com.affine.settings.workspace.byok.field.model-id": "Model ID",
|
||||||
"com.affine.settings.workspace.byok.endpoint.custom-disabled": "Custom endpoints are disabled by the server administrator. In Self-hosted Admin, enable copilot.byok.allowCustomEndpoint.",
|
"com.affine.settings.workspace.byok.endpoint.custom-disabled": "Custom endpoints are disabled by the server administrator. In Self-hosted Admin, enable copilot.byok.allowCustomEndpoint.",
|
||||||
"com.affine.settings.workspace.byok.endpoint.private-disabled": "Private network endpoints additionally require the server administrator to enable copilot.byok.allowPrivateEndpoint.",
|
"com.affine.settings.workspace.byok.endpoint.private-disabled": "Private network endpoints additionally require the server administrator to enable copilot.byok.allowPrivateEndpoint.",
|
||||||
"com.affine.settings.workspace.byok.placeholder.key-name": "Primary",
|
"com.affine.settings.workspace.byok.placeholder.keep-current-key": "Leave blank to keep the current key",
|
||||||
"com.affine.settings.workspace.byok.placeholder.description": "Workspace fallback key",
|
"com.affine.settings.workspace.byok.placeholder.model-id": "Model ID",
|
||||||
|
"com.affine.settings.workspace.byok.placeholder.search-models": "Search models…",
|
||||||
|
"com.affine.settings.workspace.byok.endpoint.use-custom": "Use a custom API-compatible endpoint",
|
||||||
|
"com.affine.settings.workspace.byok.probe.include-image": "Include an image generation request when testing (provider charges may apply)",
|
||||||
|
"com.affine.settings.workspace.byok.probe.verified": "Connection verified",
|
||||||
|
"com.affine.settings.workspace.byok.probe.failed": "Connection failed",
|
||||||
|
"com.affine.settings.workspace.byok.model.recommended": "Recommended",
|
||||||
|
"com.affine.settings.workspace.byok.model.duplicate-id": "This model has already been added.",
|
||||||
|
"com.affine.settings.workspace.byok.model.status.not-tested": "Not tested",
|
||||||
|
"com.affine.settings.workspace.byok.model.status.verified": "Verified",
|
||||||
|
"com.affine.settings.workspace.byok.model.status.failed": "Failed",
|
||||||
|
"com.affine.settings.workspace.byok.model.status.partially-verified": "Partially verified · {{verified}} of {{total}}",
|
||||||
|
"com.affine.settings.workspace.byok.model.status.disabled": "Disabled",
|
||||||
|
"com.affine.settings.workspace.byok.model.use-this-for": "Use this model for",
|
||||||
|
"com.affine.settings.workspace.byok.model.use.chat": "Chat & writing",
|
||||||
|
"com.affine.settings.workspace.byok.model.use.actions": "Actions",
|
||||||
|
"com.affine.settings.workspace.byok.model.use.structured": "Structured output",
|
||||||
|
"com.affine.settings.workspace.byok.model.use.vision": "Image understanding",
|
||||||
|
"com.affine.settings.workspace.byok.model.use.image": "Image generation",
|
||||||
|
"com.affine.settings.workspace.byok.model.use.transcript": "Transcription",
|
||||||
|
"com.affine.settings.workspace.byok.model.use.embedding": "Workspace indexing",
|
||||||
|
"com.affine.settings.workspace.byok.model.use.rerank": "Search reranking",
|
||||||
"com.affine.settings.workspace.byok.action.add-key": "Add key",
|
"com.affine.settings.workspace.byok.action.add-key": "Add key",
|
||||||
"com.affine.settings.workspace.byok.action.test-key": "Test key",
|
"com.affine.settings.workspace.byok.action.test-connection": "Test connection",
|
||||||
|
"com.affine.settings.workspace.byok.action.test": "Test",
|
||||||
|
"com.affine.settings.workspace.byok.action.testing": "Testing…",
|
||||||
"com.affine.settings.workspace.byok.action.cancel": "Cancel",
|
"com.affine.settings.workspace.byok.action.cancel": "Cancel",
|
||||||
"com.affine.settings.workspace.byok.action.save-key": "Save key",
|
"com.affine.settings.workspace.byok.action.connect": "Connect",
|
||||||
|
"com.affine.settings.workspace.byok.action.connecting": "Connecting…",
|
||||||
|
"com.affine.settings.workspace.byok.action.save-changes": "Save changes",
|
||||||
|
"com.affine.settings.workspace.byok.action.add-model": "Add model",
|
||||||
|
"com.affine.settings.workspace.byok.action.add-selected-models": "Add {{count}} models",
|
||||||
|
"com.affine.settings.workspace.byok.action.save-model": "Save model",
|
||||||
|
"com.affine.settings.workspace.byok.action.enable-model": "Enable {{model}}",
|
||||||
|
"com.affine.settings.workspace.byok.action.disable-model": "Disable {{model}}",
|
||||||
|
"com.affine.settings.workspace.byok.action.model-options": "Options for {{model}}",
|
||||||
|
"com.affine.settings.workspace.byok.action.move-up": "Move up",
|
||||||
|
"com.affine.settings.workspace.byok.action.move-down": "Move down",
|
||||||
|
"com.affine.settings.workspace.byok.action.remove": "Remove",
|
||||||
"com.affine.settings.workspace.byok.action.clear-all": "Clear all BYOK keys",
|
"com.affine.settings.workspace.byok.action.clear-all": "Clear all BYOK keys",
|
||||||
"com.affine.settings.workspace.byok.action.reorder": "Reorder",
|
"com.affine.settings.workspace.byok.action.reorder": "Reorder",
|
||||||
"com.affine.settings.workspace.byok.action.edit": "Edit",
|
"com.affine.settings.workspace.byok.action.edit": "Edit",
|
||||||
@@ -1554,6 +1599,8 @@
|
|||||||
"com.affine.settings.workspace.byok.notify.delete-failed.title": "BYOK key not deleted",
|
"com.affine.settings.workspace.byok.notify.delete-failed.title": "BYOK key not deleted",
|
||||||
"com.affine.settings.workspace.byok.notify.reorder-failed.title": "BYOK keys not reordered",
|
"com.affine.settings.workspace.byok.notify.reorder-failed.title": "BYOK keys not reordered",
|
||||||
"com.affine.settings.workspace.byok.notify.clear-failed.title": "BYOK keys not cleared",
|
"com.affine.settings.workspace.byok.notify.clear-failed.title": "BYOK keys not cleared",
|
||||||
|
"com.affine.settings.workspace.byok.notify.reload-required.title": "BYOK settings changed",
|
||||||
|
"com.affine.settings.workspace.byok.notify.reload-required.message": "Reload the settings and try again.",
|
||||||
"com.affine.settings.workspace.byok.notify.operation-failed.message": "Please try again.",
|
"com.affine.settings.workspace.byok.notify.operation-failed.message": "Please try again.",
|
||||||
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.title": "Cannot reorder across storage",
|
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.title": "Cannot reorder across storage",
|
||||||
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.message": "Local keys and server keys keep separate fallback order.",
|
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.message": "Local keys and server keys keep separate fallback order.",
|
||||||
|
|||||||
@@ -1439,25 +1439,19 @@
|
|||||||
"com.affine.settings.workspace": "Жұмыс кеңістігі",
|
"com.affine.settings.workspace": "Жұмыс кеңістігі",
|
||||||
"com.affine.settings.workspace.description": "Ағымдағы жұмыс кеңістігінің ақпаратын осы жерден көре аласың.",
|
"com.affine.settings.workspace.description": "Ағымдағы жұмыс кеңістігінің ақпаратын осы жерден көре аласың.",
|
||||||
"com.affine.settings.workspace.byok.title-beta": "AI BYOK (Бета)",
|
"com.affine.settings.workspace.byok.title-beta": "AI BYOK (Бета)",
|
||||||
"com.affine.settings.workspace.byok.title": "AI BYOK",
|
|
||||||
"com.affine.settings.workspace.byok.loading": "Провайдер кілттері жүктелуде.",
|
"com.affine.settings.workspace.byok.loading": "Провайдер кілттері жүктелуде.",
|
||||||
"com.affine.settings.workspace.byok.subtitle": "Бұл жұмыс кеңістігі үшін өз провайдер кілттеріңді пайдалан.",
|
"com.affine.settings.workspace.byok.subtitle": "Бұл жұмыс кеңістігі үшін өз провайдер кілттеріңді пайдалан.",
|
||||||
"com.affine.settings.workspace.byok.header": "AFFiNE AI жоспар бағыттарына дейін жұмыс кеңістігінің провайдер кілттерін пайдалан.",
|
"com.affine.settings.workspace.byok.header": "AFFiNE AI жоспар бағыттарына дейін жұмыс кеңістігінің провайдер кілттерін пайдалан.",
|
||||||
"com.affine.settings.workspace.byok.locked.title": "BYOK үшін Pro, Team немесе Believer қажет",
|
"com.affine.settings.workspace.byok.locked.title": "BYOK үшін Pro, Team немесе Believer қажет",
|
||||||
"com.affine.settings.workspace.byok.locked.description": "Провайдер кілттерін қосу және AFFiNE AI-ды өзіңнің OpenAI, Anthropic, Gemini немесе FAL аккаунтың арқылы бағыттау үшін осы жұмыс кеңістігін жаңарт.",
|
"com.affine.settings.workspace.byok.locked.description": "Провайдер кілттерін қосу және AFFiNE AI-ды өзіңнің OpenAI, Anthropic, Gemini немесе FAL аккаунтың арқылы бағыттау үшін осы жұмыс кеңістігін жаңарт.",
|
||||||
"com.affine.settings.workspace.byok.notice.title": "AI жоспары қолжетімді болып қалады",
|
|
||||||
"com.affine.settings.workspace.byok.notice.description": "Алдымен осы құрылғыдағы жергілікті кілттер сыналады. Одан кейін жұмыс кеңістігінің сервер кілттері, сосын квота болған кезде AFFiNE AI жоспар бағыттары пайдаланылады.",
|
|
||||||
"com.affine.settings.workspace.byok.keys.title": "Провайдер кілттері",
|
"com.affine.settings.workspace.byok.keys.title": "Провайдер кілттері",
|
||||||
"com.affine.settings.workspace.byok.keys.description": "Тізім реті әр сақтау тобы ішінде резервтік ретті басқарады.",
|
"com.affine.settings.workspace.byok.keys.description": "Тізім реті әр сақтау тобы ішінде резервтік ретті басқарады.",
|
||||||
"com.affine.settings.workspace.byok.empty.title": "Провайдер кілттері жоқ",
|
"com.affine.settings.workspace.byok.empty.title": "Провайдер кілттері жоқ",
|
||||||
"com.affine.settings.workspace.byok.empty.description": "Осы жұмыс кеңістігі үшін бірінші бағытты жасау үшін кілт қос. Кілт болмайынша провайдер жолдары көрсетілмейді.",
|
"com.affine.settings.workspace.byok.empty.description": "Осы жұмыс кеңістігі үшін бірінші бағытты жасау үшін кілт қос. Кілт болмайынша провайдер жолдары көрсетілмейді.",
|
||||||
"com.affine.settings.workspace.byok.storage.local": "Жергілікті",
|
"com.affine.settings.workspace.byok.storage.local": "Жергілікті",
|
||||||
"com.affine.settings.workspace.byok.storage.server": "Сервер",
|
"com.affine.settings.workspace.byok.storage.server": "Сервер",
|
||||||
"com.affine.settings.workspace.byok.storage.local-this-device": "Жергілікті (осы құрылғыда)",
|
|
||||||
"com.affine.settings.workspace.byok.storage.local-desktop-only": "Жергілікті (тек десктопта)",
|
|
||||||
"com.affine.settings.workspace.byok.status.disabled-after-failure": "Сәтсіздіктен кейін өшірілді",
|
"com.affine.settings.workspace.byok.status.disabled-after-failure": "Сәтсіздіктен кейін өшірілді",
|
||||||
"com.affine.settings.workspace.byok.status.key-verified": "Кілт расталды",
|
"com.affine.settings.workspace.byok.status.key-verified": "Кілт расталды",
|
||||||
"com.affine.settings.workspace.byok.status.key-test-failed": "Кілтті тексеру сәтсіз аяқталды",
|
|
||||||
"com.affine.settings.workspace.byok.capability.text": "Мәтін",
|
"com.affine.settings.workspace.byok.capability.text": "Мәтін",
|
||||||
"com.affine.settings.workspace.byok.capability.image-input": "Сурет енгізу",
|
"com.affine.settings.workspace.byok.capability.image-input": "Сурет енгізу",
|
||||||
"com.affine.settings.workspace.byok.capability.actions": "Әрекеттер",
|
"com.affine.settings.workspace.byok.capability.actions": "Әрекеттер",
|
||||||
@@ -1465,8 +1459,6 @@
|
|||||||
"com.affine.settings.workspace.byok.capability.transcript": "Транскрипт",
|
"com.affine.settings.workspace.byok.capability.transcript": "Транскрипт",
|
||||||
"com.affine.settings.workspace.byok.capability.indexing": "Индекстеу",
|
"com.affine.settings.workspace.byok.capability.indexing": "Индекстеу",
|
||||||
"com.affine.settings.workspace.byok.row.activity.failed": "{{date}} күні сәтсіз аяқталды",
|
"com.affine.settings.workspace.byok.row.activity.failed": "{{date}} күні сәтсіз аяқталды",
|
||||||
"com.affine.settings.workspace.byok.row.activity.used": "{{date}} күні қолданылды",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.used-today": "бүгін қолданылды",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.unused": "әлі қолданылмаған",
|
"com.affine.settings.workspace.byok.row.activity.unused": "әлі қолданылмаған",
|
||||||
"com.affine.settings.workspace.byok.coverage.title": "Функцияларды қамту",
|
"com.affine.settings.workspace.byok.coverage.title": "Функцияларды қамту",
|
||||||
"com.affine.settings.workspace.byok.feature.chat.title": "Жазу және чат",
|
"com.affine.settings.workspace.byok.feature.chat.title": "Жазу және чат",
|
||||||
@@ -1484,21 +1476,13 @@
|
|||||||
"com.affine.settings.workspace.byok.usage.title": "BYOK қолданылуы",
|
"com.affine.settings.workspace.byok.usage.title": "BYOK қолданылуы",
|
||||||
"com.affine.settings.workspace.byok.usage.period": "Соңғы 30 күн",
|
"com.affine.settings.workspace.byok.usage.period": "Соңғы 30 күн",
|
||||||
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} токен",
|
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} токен",
|
||||||
"com.affine.settings.workspace.byok.modal.add-title": "Провайдер кілтін қос",
|
|
||||||
"com.affine.settings.workspace.byok.modal.edit-title": "Провайдер кілтін өңде",
|
|
||||||
"com.affine.settings.workspace.byok.modal.description": "API кілтін қайта енгіз және өзгерістерді сақтамас бұрын оны тексер.",
|
|
||||||
"com.affine.settings.workspace.byok.field.provider": "Провайдер",
|
"com.affine.settings.workspace.byok.field.provider": "Провайдер",
|
||||||
"com.affine.settings.workspace.byok.field.key-name": "Кілт атауы",
|
"com.affine.settings.workspace.byok.field.key-name": "Кілт атауы",
|
||||||
"com.affine.settings.workspace.byok.field.description": "Сипаттама",
|
"com.affine.settings.workspace.byok.field.description": "Сипаттама",
|
||||||
"com.affine.settings.workspace.byok.field.storage": "Кілт сақтау орны",
|
|
||||||
"com.affine.settings.workspace.byok.field.api-key": "API кілті",
|
"com.affine.settings.workspace.byok.field.api-key": "API кілті",
|
||||||
"com.affine.settings.workspace.byok.field.endpoint": "Эндпойнт",
|
"com.affine.settings.workspace.byok.field.endpoint": "Эндпойнт",
|
||||||
"com.affine.settings.workspace.byok.placeholder.key-name": "Негізгі",
|
|
||||||
"com.affine.settings.workspace.byok.placeholder.description": "Жұмыс кеңістігінің резервтік кілті",
|
|
||||||
"com.affine.settings.workspace.byok.action.add-key": "Кілтті қос",
|
"com.affine.settings.workspace.byok.action.add-key": "Кілтті қос",
|
||||||
"com.affine.settings.workspace.byok.action.test-key": "Кілтті тексер",
|
|
||||||
"com.affine.settings.workspace.byok.action.cancel": "Бас тарт",
|
"com.affine.settings.workspace.byok.action.cancel": "Бас тарт",
|
||||||
"com.affine.settings.workspace.byok.action.save-key": "Кілтті сақта",
|
|
||||||
"com.affine.settings.workspace.byok.action.clear-all": "Барлық BYOK кілттерін тазарт",
|
"com.affine.settings.workspace.byok.action.clear-all": "Барлық BYOK кілттерін тазарт",
|
||||||
"com.affine.settings.workspace.byok.action.reorder": "Ретін өзгерт",
|
"com.affine.settings.workspace.byok.action.reorder": "Ретін өзгерт",
|
||||||
"com.affine.settings.workspace.byok.action.edit": "Өңде",
|
"com.affine.settings.workspace.byok.action.edit": "Өңде",
|
||||||
|
|||||||
@@ -1439,25 +1439,19 @@
|
|||||||
"com.affine.settings.workspace": "Çalışma alanı",
|
"com.affine.settings.workspace": "Çalışma alanı",
|
||||||
"com.affine.settings.workspace.description": "Mevcut çalışma alanının bilgilerini burada görüntüleyebilirsiniz.",
|
"com.affine.settings.workspace.description": "Mevcut çalışma alanının bilgilerini burada görüntüleyebilirsiniz.",
|
||||||
"com.affine.settings.workspace.byok.title-beta": "AI BYOK (Beta)",
|
"com.affine.settings.workspace.byok.title-beta": "AI BYOK (Beta)",
|
||||||
"com.affine.settings.workspace.byok.title": "AI BYOK",
|
|
||||||
"com.affine.settings.workspace.byok.loading": "Sağlayıcı anahtarları yükleniyor.",
|
"com.affine.settings.workspace.byok.loading": "Sağlayıcı anahtarları yükleniyor.",
|
||||||
"com.affine.settings.workspace.byok.subtitle": "Bu çalışma alanı için kendi sağlayıcı anahtarlarınızı kullanın.",
|
"com.affine.settings.workspace.byok.subtitle": "Bu çalışma alanı için kendi sağlayıcı anahtarlarınızı kullanın.",
|
||||||
"com.affine.settings.workspace.byok.header": "AFFiNE AI plan rotalarından önce çalışma alanı sağlayıcı anahtarlarını kullanın.",
|
"com.affine.settings.workspace.byok.header": "AFFiNE AI plan rotalarından önce çalışma alanı sağlayıcı anahtarlarını kullanın.",
|
||||||
"com.affine.settings.workspace.byok.locked.title": "BYOK için Pro, Team veya Believer gerekir",
|
"com.affine.settings.workspace.byok.locked.title": "BYOK için Pro, Team veya Believer gerekir",
|
||||||
"com.affine.settings.workspace.byok.locked.description": "Sağlayıcı anahtarları eklemek ve AFFiNE AI'yi kendi OpenAI, Anthropic, Gemini veya FAL hesabınız üzerinden yönlendirmek için bu çalışma alanını yükseltin.",
|
"com.affine.settings.workspace.byok.locked.description": "Sağlayıcı anahtarları eklemek ve AFFiNE AI'yi kendi OpenAI, Anthropic, Gemini veya FAL hesabınız üzerinden yönlendirmek için bu çalışma alanını yükseltin.",
|
||||||
"com.affine.settings.workspace.byok.notice.title": "Yapay zeka planı kullanılabilir durumda kalacak",
|
|
||||||
"com.affine.settings.workspace.byok.notice.description": "Önce bu cihazdaki yerel anahtarlar denenir. Çalışma alanı sunucusu anahtarları takip eder ve ardından kota mevcut olduğunda AFFiNE AI rotaları planlar.",
|
|
||||||
"com.affine.settings.workspace.byok.keys.title": "Sağlayıcı anahtarları",
|
"com.affine.settings.workspace.byok.keys.title": "Sağlayıcı anahtarları",
|
||||||
"com.affine.settings.workspace.byok.keys.description": "Her depolama grubunda yedek (fallback) sırasını liste sırası belirler.",
|
"com.affine.settings.workspace.byok.keys.description": "Her depolama grubunda yedek (fallback) sırasını liste sırası belirler.",
|
||||||
"com.affine.settings.workspace.byok.empty.title": "Sağlayıcı anahtarı yok",
|
"com.affine.settings.workspace.byok.empty.title": "Sağlayıcı anahtarı yok",
|
||||||
"com.affine.settings.workspace.byok.empty.description": "Bu çalışma alanı için ilk rotayı oluşturmak üzere bir anahtar ekleyin. Sağlayıcı satırları bir anahtar mevcut olana kadar gösterilmez.",
|
"com.affine.settings.workspace.byok.empty.description": "Bu çalışma alanı için ilk rotayı oluşturmak üzere bir anahtar ekleyin. Sağlayıcı satırları bir anahtar mevcut olana kadar gösterilmez.",
|
||||||
"com.affine.settings.workspace.byok.storage.local": "Yerel",
|
"com.affine.settings.workspace.byok.storage.local": "Yerel",
|
||||||
"com.affine.settings.workspace.byok.storage.server": "Sunucu",
|
"com.affine.settings.workspace.byok.storage.server": "Sunucu",
|
||||||
"com.affine.settings.workspace.byok.storage.local-this-device": "Yerel (bu cihaz)",
|
|
||||||
"com.affine.settings.workspace.byok.storage.local-desktop-only": "Yerel (Yalnızca masaüstü)",
|
|
||||||
"com.affine.settings.workspace.byok.status.disabled-after-failure": "Arızadan sonra devre dışı bırakıldı",
|
"com.affine.settings.workspace.byok.status.disabled-after-failure": "Arızadan sonra devre dışı bırakıldı",
|
||||||
"com.affine.settings.workspace.byok.status.key-verified": "Anahtar doğrulandı",
|
"com.affine.settings.workspace.byok.status.key-verified": "Anahtar doğrulandı",
|
||||||
"com.affine.settings.workspace.byok.status.key-test-failed": "Anahtar testi başarısız oldu",
|
|
||||||
"com.affine.settings.workspace.byok.capability.text": "Metin",
|
"com.affine.settings.workspace.byok.capability.text": "Metin",
|
||||||
"com.affine.settings.workspace.byok.capability.image-input": "Görüntü girişi",
|
"com.affine.settings.workspace.byok.capability.image-input": "Görüntü girişi",
|
||||||
"com.affine.settings.workspace.byok.capability.actions": "Eylemler",
|
"com.affine.settings.workspace.byok.capability.actions": "Eylemler",
|
||||||
@@ -1465,8 +1459,6 @@
|
|||||||
"com.affine.settings.workspace.byok.capability.transcript": "Deşifre metni",
|
"com.affine.settings.workspace.byok.capability.transcript": "Deşifre metni",
|
||||||
"com.affine.settings.workspace.byok.capability.indexing": "İndeksleme",
|
"com.affine.settings.workspace.byok.capability.indexing": "İndeksleme",
|
||||||
"com.affine.settings.workspace.byok.row.activity.failed": "başarısız oldu {{date}}",
|
"com.affine.settings.workspace.byok.row.activity.failed": "başarısız oldu {{date}}",
|
||||||
"com.affine.settings.workspace.byok.row.activity.used": "{{date}} kullanıldı",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.used-today": "Bugün kullanıldı",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.unused": "henüz kullanılmadı",
|
"com.affine.settings.workspace.byok.row.activity.unused": "henüz kullanılmadı",
|
||||||
"com.affine.settings.workspace.byok.coverage.title": "Özellik kapsamı",
|
"com.affine.settings.workspace.byok.coverage.title": "Özellik kapsamı",
|
||||||
"com.affine.settings.workspace.byok.feature.chat.title": "Yazma ve sohbet",
|
"com.affine.settings.workspace.byok.feature.chat.title": "Yazma ve sohbet",
|
||||||
@@ -1484,21 +1476,13 @@
|
|||||||
"com.affine.settings.workspace.byok.usage.title": "BYOK kullanımı",
|
"com.affine.settings.workspace.byok.usage.title": "BYOK kullanımı",
|
||||||
"com.affine.settings.workspace.byok.usage.period": "Son 30 gün",
|
"com.affine.settings.workspace.byok.usage.period": "Son 30 gün",
|
||||||
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} jeton",
|
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} jeton",
|
||||||
"com.affine.settings.workspace.byok.modal.add-title": "Sağlayıcı anahtarı ekle",
|
|
||||||
"com.affine.settings.workspace.byok.modal.edit-title": "Sağlayıcı anahtarını düzenle",
|
|
||||||
"com.affine.settings.workspace.byok.modal.description": "Değişiklikleri kaydetmeden önce API anahtarını tekrar girin ve test edin.",
|
|
||||||
"com.affine.settings.workspace.byok.field.provider": "Sağlayıcı",
|
"com.affine.settings.workspace.byok.field.provider": "Sağlayıcı",
|
||||||
"com.affine.settings.workspace.byok.field.key-name": "Anahtar adı",
|
"com.affine.settings.workspace.byok.field.key-name": "Anahtar adı",
|
||||||
"com.affine.settings.workspace.byok.field.description": "Tanım",
|
"com.affine.settings.workspace.byok.field.description": "Tanım",
|
||||||
"com.affine.settings.workspace.byok.field.storage": "Anahtar saklama",
|
|
||||||
"com.affine.settings.workspace.byok.field.api-key": "API anahtarı",
|
"com.affine.settings.workspace.byok.field.api-key": "API anahtarı",
|
||||||
"com.affine.settings.workspace.byok.field.endpoint": "Uç nokta",
|
"com.affine.settings.workspace.byok.field.endpoint": "Uç nokta",
|
||||||
"com.affine.settings.workspace.byok.placeholder.key-name": "Birincil",
|
|
||||||
"com.affine.settings.workspace.byok.placeholder.description": "Çalışma alanı yedek anahtarı",
|
|
||||||
"com.affine.settings.workspace.byok.action.add-key": "Anahtar ekle",
|
"com.affine.settings.workspace.byok.action.add-key": "Anahtar ekle",
|
||||||
"com.affine.settings.workspace.byok.action.test-key": "Test anahtarı",
|
|
||||||
"com.affine.settings.workspace.byok.action.cancel": "İptal",
|
"com.affine.settings.workspace.byok.action.cancel": "İptal",
|
||||||
"com.affine.settings.workspace.byok.action.save-key": "Anahtarı kaydet",
|
|
||||||
"com.affine.settings.workspace.byok.action.clear-all": "Tüm BYOK anahtarlarını temizle",
|
"com.affine.settings.workspace.byok.action.clear-all": "Tüm BYOK anahtarlarını temizle",
|
||||||
"com.affine.settings.workspace.byok.action.reorder": "Yeniden sırala",
|
"com.affine.settings.workspace.byok.action.reorder": "Yeniden sırala",
|
||||||
"com.affine.settings.workspace.byok.action.edit": "Düzenle",
|
"com.affine.settings.workspace.byok.action.edit": "Düzenle",
|
||||||
|
|||||||
@@ -1735,8 +1735,6 @@
|
|||||||
"com.affine.settings.workspace.byok.action.delete": "حذف کریں۔",
|
"com.affine.settings.workspace.byok.action.delete": "حذف کریں۔",
|
||||||
"com.affine.settings.workspace.byok.action.edit": "ترمیم کریں۔",
|
"com.affine.settings.workspace.byok.action.edit": "ترمیم کریں۔",
|
||||||
"com.affine.settings.workspace.byok.action.reorder": "دوبارہ ترتیب دیں۔",
|
"com.affine.settings.workspace.byok.action.reorder": "دوبارہ ترتیب دیں۔",
|
||||||
"com.affine.settings.workspace.byok.action.save-key": "کلید محفوظ کریں۔",
|
|
||||||
"com.affine.settings.workspace.byok.action.test-key": "ٹیسٹ کلید",
|
|
||||||
"com.affine.settings.workspace.byok.capability.actions": "اعمال",
|
"com.affine.settings.workspace.byok.capability.actions": "اعمال",
|
||||||
"com.affine.settings.workspace.byok.capability.image-generate": "تصویر بنائیں",
|
"com.affine.settings.workspace.byok.capability.image-generate": "تصویر بنائیں",
|
||||||
"com.affine.settings.workspace.byok.capability.image-input": "امیج ان پٹ",
|
"com.affine.settings.workspace.byok.capability.image-input": "امیج ان پٹ",
|
||||||
@@ -1761,18 +1759,12 @@
|
|||||||
"com.affine.settings.workspace.byok.field.endpoint": "اختتامی نقطہ",
|
"com.affine.settings.workspace.byok.field.endpoint": "اختتامی نقطہ",
|
||||||
"com.affine.settings.workspace.byok.field.key-name": "کلیدی نام",
|
"com.affine.settings.workspace.byok.field.key-name": "کلیدی نام",
|
||||||
"com.affine.settings.workspace.byok.field.provider": "فراہم کرنے والا",
|
"com.affine.settings.workspace.byok.field.provider": "فراہم کرنے والا",
|
||||||
"com.affine.settings.workspace.byok.field.storage": "کلیدی ذخیرہ",
|
|
||||||
"com.affine.settings.workspace.byok.header": "AFFiNE AI پلان کے راستوں سے پہلے ورک اسپیس فراہم کنندہ کیز استعمال کریں۔",
|
"com.affine.settings.workspace.byok.header": "AFFiNE AI پلان کے راستوں سے پہلے ورک اسپیس فراہم کنندہ کیز استعمال کریں۔",
|
||||||
"com.affine.settings.workspace.byok.keys.description": "فہرست آرڈر کنٹرولز فال بیک ہر اسٹوریج گروپ کے اندر۔",
|
"com.affine.settings.workspace.byok.keys.description": "فہرست آرڈر کنٹرولز فال بیک ہر اسٹوریج گروپ کے اندر۔",
|
||||||
"com.affine.settings.workspace.byok.keys.title": "فراہم کنندہ کی چابیاں",
|
"com.affine.settings.workspace.byok.keys.title": "فراہم کنندہ کی چابیاں",
|
||||||
"com.affine.settings.workspace.byok.loading": "فراہم کنندہ کی چابیاں لوڈ ہو رہی ہیں۔",
|
"com.affine.settings.workspace.byok.loading": "فراہم کنندہ کی چابیاں لوڈ ہو رہی ہیں۔",
|
||||||
"com.affine.settings.workspace.byok.locked.description": "فراہم کنندہ کیز شامل کرنے کے لیے اس ورک اسپیس کو اپ گریڈ کریں اور AFFiNE AI کو اپنے OpenAI، Anthropic، Gemini، یا FAL اکاؤنٹ کے ذریعے روٹ کریں۔",
|
"com.affine.settings.workspace.byok.locked.description": "فراہم کنندہ کیز شامل کرنے کے لیے اس ورک اسپیس کو اپ گریڈ کریں اور AFFiNE AI کو اپنے OpenAI، Anthropic، Gemini، یا FAL اکاؤنٹ کے ذریعے روٹ کریں۔",
|
||||||
"com.affine.settings.workspace.byok.locked.title": "BYOK کو پرو، ٹیم، یا مومن درکار ہے۔",
|
"com.affine.settings.workspace.byok.locked.title": "BYOK کو پرو، ٹیم، یا مومن درکار ہے۔",
|
||||||
"com.affine.settings.workspace.byok.modal.add-title": "فراہم کنندہ کلید شامل کریں۔",
|
|
||||||
"com.affine.settings.workspace.byok.modal.description": "API کلید دوبارہ درج کریں اور تبدیلیاں محفوظ کرنے سے پہلے اس کی جانچ کریں۔",
|
|
||||||
"com.affine.settings.workspace.byok.modal.edit-title": "فراہم کنندہ کلید میں ترمیم کریں۔",
|
|
||||||
"com.affine.settings.workspace.byok.notice.description": "اس آلہ پر مقامی کلیدیں پہلے آزمائی جاتی ہیں۔ ورک اسپیس سرور کیز فالو کرتی ہیں، پھر کوٹہ دستیاب ہونے پر AFFiNE AI پلان روٹس۔",
|
|
||||||
"com.affine.settings.workspace.byok.notice.title": "AI پلان دستیاب رہتا ہے۔",
|
|
||||||
"com.affine.settings.workspace.byok.notify.clear-failed.title": "BYOK کیز صاف نہیں ہوئیں",
|
"com.affine.settings.workspace.byok.notify.clear-failed.title": "BYOK کیز صاف نہیں ہوئیں",
|
||||||
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.message": "لوکل کیز اور سرور کیز الگ الگ فال بیک آرڈر رکھتی ہیں۔",
|
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.message": "لوکل کیز اور سرور کیز الگ الگ فال بیک آرڈر رکھتی ہیں۔",
|
||||||
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.title": "پورے اسٹوریج میں دوبارہ ترتیب نہیں دی جا سکتی",
|
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.title": "پورے اسٹوریج میں دوبارہ ترتیب نہیں دی جا سکتی",
|
||||||
@@ -1784,21 +1776,13 @@
|
|||||||
"com.affine.settings.workspace.byok.notify.reorder-failed.title": "BYOK کیز دوبارہ ترتیب نہیں دی گئیں۔",
|
"com.affine.settings.workspace.byok.notify.reorder-failed.title": "BYOK کیز دوبارہ ترتیب نہیں دی گئیں۔",
|
||||||
"com.affine.settings.workspace.byok.notify.save-failed.title": "BYOK کلید محفوظ نہیں ہے۔",
|
"com.affine.settings.workspace.byok.notify.save-failed.title": "BYOK کلید محفوظ نہیں ہے۔",
|
||||||
"com.affine.settings.workspace.byok.notify.test-failed.title": "کلیدی ٹیسٹ ناکام ہو گیا۔",
|
"com.affine.settings.workspace.byok.notify.test-failed.title": "کلیدی ٹیسٹ ناکام ہو گیا۔",
|
||||||
"com.affine.settings.workspace.byok.placeholder.description": "ورک اسپیس فال بیک کلید",
|
|
||||||
"com.affine.settings.workspace.byok.placeholder.key-name": "پرائمری",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.failed": "ناکام {{date}}",
|
"com.affine.settings.workspace.byok.row.activity.failed": "ناکام {{date}}",
|
||||||
"com.affine.settings.workspace.byok.row.activity.unused": "ابھی تک استعمال نہیں کیا",
|
"com.affine.settings.workspace.byok.row.activity.unused": "ابھی تک استعمال نہیں کیا",
|
||||||
"com.affine.settings.workspace.byok.row.activity.used": "استعمال شدہ {{date}}",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.used-today": "آج استعمال کیا جاتا ہے",
|
|
||||||
"com.affine.settings.workspace.byok.status.disabled-after-failure": "ناکامی کے بعد معذور",
|
"com.affine.settings.workspace.byok.status.disabled-after-failure": "ناکامی کے بعد معذور",
|
||||||
"com.affine.settings.workspace.byok.status.key-test-failed": "کلیدی ٹیسٹ ناکام ہو گیا۔",
|
|
||||||
"com.affine.settings.workspace.byok.status.key-verified": "کلید کی تصدیق ہو گئی۔",
|
"com.affine.settings.workspace.byok.status.key-verified": "کلید کی تصدیق ہو گئی۔",
|
||||||
"com.affine.settings.workspace.byok.storage.local": "مقامی",
|
"com.affine.settings.workspace.byok.storage.local": "مقامی",
|
||||||
"com.affine.settings.workspace.byok.storage.local-desktop-only": "مقامی (صرف ڈیسک ٹاپ)",
|
|
||||||
"com.affine.settings.workspace.byok.storage.local-this-device": "مقامی (یہ آلہ)",
|
|
||||||
"com.affine.settings.workspace.byok.storage.server": "سرور",
|
"com.affine.settings.workspace.byok.storage.server": "سرور",
|
||||||
"com.affine.settings.workspace.byok.subtitle": "اس ورک اسپیس کے لیے اپنی فراہم کنندہ کیز استعمال کریں۔",
|
"com.affine.settings.workspace.byok.subtitle": "اس ورک اسپیس کے لیے اپنی فراہم کنندہ کیز استعمال کریں۔",
|
||||||
"com.affine.settings.workspace.byok.title": "AI BYOK",
|
|
||||||
"com.affine.settings.workspace.byok.title-beta": "AI BYOK (بیٹا)",
|
"com.affine.settings.workspace.byok.title-beta": "AI BYOK (بیٹا)",
|
||||||
"com.affine.settings.workspace.byok.usage.period": "آخری 30 دن",
|
"com.affine.settings.workspace.byok.usage.period": "آخری 30 دن",
|
||||||
"com.affine.settings.workspace.byok.usage.title": "BYOK کا استعمال",
|
"com.affine.settings.workspace.byok.usage.title": "BYOK کا استعمال",
|
||||||
|
|||||||
@@ -1478,25 +1478,19 @@
|
|||||||
"com.affine.settings.workspace": "工作区",
|
"com.affine.settings.workspace": "工作区",
|
||||||
"com.affine.settings.workspace.description": "您可以在此处自定义您的工作区。",
|
"com.affine.settings.workspace.description": "您可以在此处自定义您的工作区。",
|
||||||
"com.affine.settings.workspace.byok.title-beta": "AI BYOK(Beta)",
|
"com.affine.settings.workspace.byok.title-beta": "AI BYOK(Beta)",
|
||||||
"com.affine.settings.workspace.byok.title": "AI BYOK",
|
|
||||||
"com.affine.settings.workspace.byok.loading": "正在加载服务商密钥。",
|
"com.affine.settings.workspace.byok.loading": "正在加载服务商密钥。",
|
||||||
"com.affine.settings.workspace.byok.subtitle": "为此工作区使用您自己的服务商密钥。",
|
"com.affine.settings.workspace.byok.subtitle": "为此工作区使用您自己的服务商密钥。",
|
||||||
"com.affine.settings.workspace.byok.header": "优先使用工作区服务商密钥,再回退到 AFFiNE AI 套餐通道。",
|
"com.affine.settings.workspace.byok.header": "优先使用工作区服务商密钥,再回退到 AFFiNE AI 套餐通道。",
|
||||||
"com.affine.settings.workspace.byok.locked.title": "BYOK 需要 Pro、Team 或 Believer",
|
"com.affine.settings.workspace.byok.locked.title": "BYOK 需要 Pro、Team 或 Believer",
|
||||||
"com.affine.settings.workspace.byok.locked.description": "升级此工作区后,可添加服务商密钥,并通过您自己的 OpenAI、Anthropic、Gemini 或 FAL 账号路由 AFFiNE AI。",
|
"com.affine.settings.workspace.byok.locked.description": "升级此工作区后,可添加服务商密钥,并通过您自己的 OpenAI、Anthropic、Gemini 或 FAL 账号路由 AFFiNE AI。",
|
||||||
"com.affine.settings.workspace.byok.notice.title": "AI 套餐仍可用",
|
|
||||||
"com.affine.settings.workspace.byok.notice.description": "会先尝试此设备上的本地密钥,然后使用工作区服务端密钥,最后在额度可用时回退到 AFFiNE AI 套餐通道。",
|
|
||||||
"com.affine.settings.workspace.byok.keys.title": "服务商密钥",
|
"com.affine.settings.workspace.byok.keys.title": "服务商密钥",
|
||||||
"com.affine.settings.workspace.byok.keys.description": "列表顺序控制每个存储分组内的回退顺序。",
|
"com.affine.settings.workspace.byok.keys.description": "列表顺序控制每个存储分组内的回退顺序。",
|
||||||
"com.affine.settings.workspace.byok.empty.title": "暂无服务商密钥",
|
"com.affine.settings.workspace.byok.empty.title": "暂无服务商密钥",
|
||||||
"com.affine.settings.workspace.byok.empty.description": "添加密钥后,将为此工作区创建第一条路由。创建前不会显示服务商行。",
|
"com.affine.settings.workspace.byok.empty.description": "添加密钥后,将为此工作区创建第一条路由。创建前不会显示服务商行。",
|
||||||
"com.affine.settings.workspace.byok.storage.local": "本地",
|
"com.affine.settings.workspace.byok.storage.local": "本地",
|
||||||
"com.affine.settings.workspace.byok.storage.server": "服务端",
|
"com.affine.settings.workspace.byok.storage.server": "服务端",
|
||||||
"com.affine.settings.workspace.byok.storage.local-this-device": "本地(此设备)",
|
|
||||||
"com.affine.settings.workspace.byok.storage.local-desktop-only": "本地(仅桌面端)",
|
|
||||||
"com.affine.settings.workspace.byok.status.disabled-after-failure": "失败后已禁用",
|
"com.affine.settings.workspace.byok.status.disabled-after-failure": "失败后已禁用",
|
||||||
"com.affine.settings.workspace.byok.status.key-verified": "密钥已验证",
|
"com.affine.settings.workspace.byok.status.key-verified": "密钥已验证",
|
||||||
"com.affine.settings.workspace.byok.status.key-test-failed": "密钥测试失败",
|
|
||||||
"com.affine.settings.workspace.byok.capability.text": "文本",
|
"com.affine.settings.workspace.byok.capability.text": "文本",
|
||||||
"com.affine.settings.workspace.byok.capability.image-input": "图片输入",
|
"com.affine.settings.workspace.byok.capability.image-input": "图片输入",
|
||||||
"com.affine.settings.workspace.byok.capability.actions": "动作",
|
"com.affine.settings.workspace.byok.capability.actions": "动作",
|
||||||
@@ -1504,8 +1498,6 @@
|
|||||||
"com.affine.settings.workspace.byok.capability.transcript": "转录",
|
"com.affine.settings.workspace.byok.capability.transcript": "转录",
|
||||||
"com.affine.settings.workspace.byok.capability.indexing": "索引",
|
"com.affine.settings.workspace.byok.capability.indexing": "索引",
|
||||||
"com.affine.settings.workspace.byok.row.activity.failed": "{{date}} 失败",
|
"com.affine.settings.workspace.byok.row.activity.failed": "{{date}} 失败",
|
||||||
"com.affine.settings.workspace.byok.row.activity.used": "{{date}} 使用",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.used-today": "今天使用",
|
|
||||||
"com.affine.settings.workspace.byok.row.activity.unused": "尚未使用",
|
"com.affine.settings.workspace.byok.row.activity.unused": "尚未使用",
|
||||||
"com.affine.settings.workspace.byok.coverage.title": "功能覆盖",
|
"com.affine.settings.workspace.byok.coverage.title": "功能覆盖",
|
||||||
"com.affine.settings.workspace.byok.feature.chat.title": "写作与聊天",
|
"com.affine.settings.workspace.byok.feature.chat.title": "写作与聊天",
|
||||||
@@ -1523,23 +1515,17 @@
|
|||||||
"com.affine.settings.workspace.byok.usage.title": "BYOK 用量",
|
"com.affine.settings.workspace.byok.usage.title": "BYOK 用量",
|
||||||
"com.affine.settings.workspace.byok.usage.period": "最近 30 天",
|
"com.affine.settings.workspace.byok.usage.period": "最近 30 天",
|
||||||
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} tokens",
|
"com.affine.settings.workspace.byok.usage.tokens": "{{count}} tokens",
|
||||||
"com.affine.settings.workspace.byok.modal.add-title": "添加服务商密钥",
|
|
||||||
"com.affine.settings.workspace.byok.modal.edit-title": "编辑服务商密钥",
|
|
||||||
"com.affine.settings.workspace.byok.modal.description": "重新输入 API key 并测试通过后才能保存更改。",
|
|
||||||
"com.affine.settings.workspace.byok.field.provider": "服务商",
|
"com.affine.settings.workspace.byok.field.provider": "服务商",
|
||||||
"com.affine.settings.workspace.byok.field.key-name": "密钥名称",
|
"com.affine.settings.workspace.byok.field.key-name": "密钥名称",
|
||||||
"com.affine.settings.workspace.byok.field.description": "描述",
|
"com.affine.settings.workspace.byok.field.description": "描述",
|
||||||
"com.affine.settings.workspace.byok.field.storage": "密钥存储",
|
|
||||||
"com.affine.settings.workspace.byok.field.api-key": "API key",
|
"com.affine.settings.workspace.byok.field.api-key": "API key",
|
||||||
"com.affine.settings.workspace.byok.field.endpoint": "Endpoint",
|
"com.affine.settings.workspace.byok.field.endpoint": "Endpoint",
|
||||||
"com.affine.settings.workspace.byok.endpoint.custom-disabled": "服务器管理员已禁用自定义端点。请在自托管管理后台启用 copilot.byok.allowCustomEndpoint。",
|
"com.affine.settings.workspace.byok.endpoint.custom-disabled": "服务器管理员已禁用自定义端点。请在自托管管理后台启用 copilot.byok.allowCustomEndpoint。",
|
||||||
"com.affine.settings.workspace.byok.endpoint.private-disabled": "私有网络端点还需要服务器管理员启用 copilot.byok.allowPrivateEndpoint。",
|
"com.affine.settings.workspace.byok.endpoint.private-disabled": "私有网络端点还需要服务器管理员启用 copilot.byok.allowPrivateEndpoint。",
|
||||||
"com.affine.settings.workspace.byok.placeholder.key-name": "Primary",
|
|
||||||
"com.affine.settings.workspace.byok.placeholder.description": "工作区回退密钥",
|
|
||||||
"com.affine.settings.workspace.byok.action.add-key": "添加密钥",
|
"com.affine.settings.workspace.byok.action.add-key": "添加密钥",
|
||||||
"com.affine.settings.workspace.byok.action.test-key": "测试密钥",
|
"com.affine.settings.workspace.byok.action.test": "测试",
|
||||||
|
"com.affine.settings.workspace.byok.action.testing": "测试中…",
|
||||||
"com.affine.settings.workspace.byok.action.cancel": "取消",
|
"com.affine.settings.workspace.byok.action.cancel": "取消",
|
||||||
"com.affine.settings.workspace.byok.action.save-key": "保存密钥",
|
|
||||||
"com.affine.settings.workspace.byok.action.clear-all": "清空所有 BYOK 密钥",
|
"com.affine.settings.workspace.byok.action.clear-all": "清空所有 BYOK 密钥",
|
||||||
"com.affine.settings.workspace.byok.action.reorder": "重新排序",
|
"com.affine.settings.workspace.byok.action.reorder": "重新排序",
|
||||||
"com.affine.settings.workspace.byok.action.edit": "编辑",
|
"com.affine.settings.workspace.byok.action.edit": "编辑",
|
||||||
@@ -1552,6 +1538,8 @@
|
|||||||
"com.affine.settings.workspace.byok.notify.delete-failed.title": "BYOK 密钥未删除",
|
"com.affine.settings.workspace.byok.notify.delete-failed.title": "BYOK 密钥未删除",
|
||||||
"com.affine.settings.workspace.byok.notify.reorder-failed.title": "BYOK 密钥未重新排序",
|
"com.affine.settings.workspace.byok.notify.reorder-failed.title": "BYOK 密钥未重新排序",
|
||||||
"com.affine.settings.workspace.byok.notify.clear-failed.title": "BYOK 密钥未清空",
|
"com.affine.settings.workspace.byok.notify.clear-failed.title": "BYOK 密钥未清空",
|
||||||
|
"com.affine.settings.workspace.byok.notify.reload-required.title": "BYOK 设置已变化",
|
||||||
|
"com.affine.settings.workspace.byok.notify.reload-required.message": "请重新加载设置后再试。",
|
||||||
"com.affine.settings.workspace.byok.notify.operation-failed.message": "请稍后重试。",
|
"com.affine.settings.workspace.byok.notify.operation-failed.message": "请稍后重试。",
|
||||||
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.title": "不能跨存储排序",
|
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.title": "不能跨存储排序",
|
||||||
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.message": "本地密钥和服务端密钥会保留各自独立的回退顺序。",
|
"com.affine.settings.workspace.byok.notify.cross-storage-reorder.message": "本地密钥和服务端密钥会保留各自独立的回退顺序。",
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ const config: PlaywrightTestConfig = {
|
|||||||
webServer: [
|
webServer: [
|
||||||
{
|
{
|
||||||
command: 'yarn run -T affine dev -p @affine/web',
|
command: 'yarn run -T affine dev -p @affine/web',
|
||||||
stdout: 'ignore',
|
stdout: 'pipe',
|
||||||
stderr: 'ignore',
|
stderr: 'pipe',
|
||||||
timeout: 120 * 1000,
|
timeout: 240 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
env: {
|
env: {
|
||||||
COVERAGE: process.env.COVERAGE || 'false',
|
COVERAGE: process.env.COVERAGE || 'false',
|
||||||
@@ -40,10 +40,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: 'yarn run -T affine dev -p @affine/server',
|
command: 'yarn run -T affine dev -p @affine/server',
|
||||||
timeout: 120 * 1000,
|
timeout: 240 * 1000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
stdout: 'ignore',
|
stdout: 'pipe',
|
||||||
stderr: 'ignore',
|
stderr: 'pipe',
|
||||||
env: {
|
env: {
|
||||||
DATABASE_URL:
|
DATABASE_URL:
|
||||||
process.env.DATABASE_URL ??
|
process.env.DATABASE_URL ??
|
||||||
|
|||||||
Reference in New Issue
Block a user